From fe9e5b6b8932b246d3b3a768ddd9ab2d16ad0faf Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Wed, 8 Apr 2026 20:29:18 +0200 Subject: [PATCH] cleaned sql and ui language sets --- TOPICS.md | 1 + b-reference/frontend-nyla/architecture.md | 12 +- b-reference/gateway/architecture.md | 10 +- c-work/1-plan/2026-04-analysis-editor.md | 398 ---------- ...6-04-gateway-int-stability-and-bugfixes.md | 123 ++++ .../2-build/2026-04-generic-graph-editor.md | 694 ++++++++++++++++++ .../2026-04-ui-i18n-dynamic-language-sets.md | 195 +++++ d-guides/coding-conventions.md | 26 +- d-guides/doc-sync.mdc | 1 + 9 files changed, 1052 insertions(+), 408 deletions(-) delete mode 100644 c-work/1-plan/2026-04-analysis-editor.md create mode 100644 c-work/1-plan/2026-04-gateway-int-stability-and-bugfixes.md create mode 100644 c-work/2-build/2026-04-generic-graph-editor.md create mode 100644 c-work/4-done/2026-04-ui-i18n-dynamic-language-sets.md diff --git a/TOPICS.md b/TOPICS.md index 751a12c..847ca9f 100644 --- a/TOPICS.md +++ b/TOPICS.md @@ -42,6 +42,7 @@ Lade immer zuerst diese Datei. Dann gezielt die passende(n) Referenz-Datei(en). |-------|-------|------------| | Automation Unification | c-work/1-plan/2026-04-automation-unification.md | Refactoring v1/v2/Workspace | | Web Image Search | c-work/1-plan/2026-03-web-image-search.md | WEB_SEARCH_MEDIA Feature | +| UI i18n / Sprachsets (done) | c-work/3-validate/2026-04-ui-i18n-dynamic-language-sets.md | Mehrsprachigkeit, `t()`, Sprachset-API, Admin-UI, AI-Übersetzung | ## Prozess & Betrieb diff --git a/b-reference/frontend-nyla/architecture.md b/b-reference/frontend-nyla/architecture.md index 2e0f45e..36e98ca 100644 --- a/b-reference/frontend-nyla/architecture.md +++ b/b-reference/frontend-nyla/architecture.md @@ -1,5 +1,5 @@ - + # Frontend Nyla -- Architektur @@ -21,7 +21,7 @@ Technologie-Stack (Stand UI-Doku): React 19.x, Vite 5.x, TypeScript 5.8.x, React | `api/` | API-Client (`api.ts`) und Feature-spezifische API-Module | | `core/` | PageManager | | `layouts/` | Layout-Komponenten | -| `locales/` | i18n | +| `locales/` | i18n: API-basiertes Language-Loading (`index.ts`, `types.ts`), keine statischen Locale-Files | | `types/` | TypeScript-Typen | | `utils/` | Utility-Funktionen | @@ -49,11 +49,13 @@ Ergänzend typische Root-Dateien und Bereiche im Repo: `main.tsx`, `App.tsx`, `a - **Geschützte Bereiche:** Route-Guards (z. B. `ProtectedRoute`) prüfen Authentifizierung; Redirect bei fehlender Session. - **Haupt-App:** Nach Login **`MainLayout.tsx`** mit **`MandateNavigation`** (Sidebar) und `` (React Router). - **Seiten-Mapping:** `pageRegistry.tsx` definiert `PAGE_REGISTRY` und `FEATURE_REGISTRY`; Seiten werden lazy geladen. `core/PageManager/` enthält ergänzende Infrastruktur (State Preservation, Lifecycle-Hooks). -- **i18n / Theme:** global über Context; API-Requests mit Auth-Header (Interceptor in zentralem API-Client). +- **i18n:** DB-backed via `LanguageContext` (`t()`-Hook). Sprachsets werden dynamisch via public API geladen (`GET /api/i18n/sets/{code}`). Key-Konvention: **Deutscher Klartext = Key**. Jeder neue/geänderte UI-Text MUSS mit `t('Deutscher Klartext')` getaggt werden. Variable Interpolation: `t('Text {var}', {var: 'Wert'})`. Fallback: Ziel-Set → `de`-Set → Key selbst. Keine statischen Locale-Files. +- **Theme:** global über Context; API-Requests mit Auth-Header (Interceptor in zentralem API-Client). ## UI-Regeln - **Keine Browser-Dialoge** (`alert` / `confirm` / `prompt`) — stattdessen `useConfirm()` und `usePrompt()` Hooks +- **i18n-Pflicht:** Jeder UI-Text (Label, Button, Placeholder, Tooltip, Fehlermeldung) MUSS mit `t('Deutscher Klartext')` getaggt werden. Hardcodierte deutsche Strings im JSX sind nicht erlaubt. Import: `const { t } = useLanguage();` - Alle internen Funktionen mit `_` Prefix - camelCase für Variablen und Funktionen @@ -76,7 +78,9 @@ Ergänzend typische Root-Dateien und Bereiche im Repo: `main.tsx`, `App.tsx`, `a | `components/FlowEditor/editor/CanvasHeader.tsx` | Versioning, Template-Management, Workflow-Aktionen | | `components/FlowEditor/editor/TemplatePicker.tsx` | Template-Auswahl-Modal | | `pages/views/graphicalEditor/GraphicalEditorPage.tsx` | Feature-Seite mit KeepAlive, URL-basiertem Workflow-Loading | -| `locales/*` | Übersetzungen (z. B. de / en / fr) | +| `locales/index.ts`, `types.ts` | i18n: API-basiertes Language-Loading (DB-backed, keine statischen Files) | +| `providers/language/LanguageContext.tsx` | `t()`-Hook mit `{variable}`-Interpolation, Fallback-Kette, `availableLanguages` | +| `pages/admin/AdminLanguagesPage.tsx` | Admin-Seite: Sprachset-Verwaltung (CRUD, AI-Übersetzung) | ## Regeln / Invarianten diff --git a/b-reference/gateway/architecture.md b/b-reference/gateway/architecture.md index 3524f67..808fe06 100644 --- a/b-reference/gateway/architecture.md +++ b/b-reference/gateway/architecture.md @@ -1,5 +1,5 @@ - + # Gateway -- Architektur @@ -17,11 +17,11 @@ Unter `gateway/modules/` (Kontext-Audit): | `aicore/` | Model-Registry, Model-Selector, Provider-Plugins (Anthropic, OpenAI, Mistral, Perplexity, Tavily, PrivateLLM) | | `auth/` | Authentifizierung, CSRF, Token-Refresh-Middleware, JWT | | `connectors/` | DB-Connector (PostgreSQL), Provider-Subpakete (Microsoft, Google, ClickUp, FTP), Ticket/Messaging/Geo-Konnektoren | -| `datamodels/` | Pydantic-Datenmodelle (u. a. Ai, Billing, Chat, Content, Files, Knowledge, Rbac, Subscription, Workflow) | +| `datamodels/` | Pydantic-Datenmodelle (u. a. Ai, Billing, Chat, Content, Files, Knowledge, Rbac, Subscription, UiLanguage, Workflow) | | `features/` | Feature-Module (autonome Domänen): workspace, graphicalEditor, chatbot, commcoach, neutralization, realEstate, trustee, teamsbot | | `interfaces/` | DB-Interfaces (App, Billing, Chat, Knowledge, Management, Subscription), AI-Objects, RBAC, Features, Messaging | | `migration/` | Daten-Migrationen | -| `routes/` | REST-API-Routen (u. a. Admin, Billing, DataFiles, DataSources, Security, Store, System) | +| `routes/` | REST-API-Routen (u. a. Admin, Billing, DataFiles, DataSources, i18n, Security, Store, System) | | `security/` | RBAC (`rbac.py`, `rbacCatalog.py`), Root-Access | | `serviceCenter/` | Zentrale Service-Orchestrierung (Registry, Resolver, Kontext, Haupt-Services) | | `serviceHub/` | Service-Registry und Dependency Injection (u. a. `PublicService`-Wrapper) | @@ -70,7 +70,7 @@ Die genannten Module kapseln die Datenbankzugriffe bzw. die zugehörigen Fachver | `interfaceDbBilling.py` | BillingAccount, BillingTransaction, Subscriptions | | `interfaceDbChat.py` | ChatWorkflow, ChatMessage, ChatDocument | | `interfaceDbKnowledge.py` | FileContentIndex, ContentChunk, RoundMemory (RAG/Knowledge Store) | -| `interfaceDbManagement.py` | FileItem, Folder, Prompt, DataSource (mandantenweite Stammdaten) | +| `interfaceDbManagement.py` | FileItem, Folder, Prompt, DataSource (mandantenweite Stammdaten), UiLanguageSet-Seeding | | `interfaceDbSubscription.py` | Subscription-Verwaltung | | `interfaceAiObjects.py` | AI-Call-Abstraktion (Text, Embedding, Vision, Streaming) | | `interfaceFeatures.py` | Feature-Instanz-Lifecycle, Template-Rollen-Kopie | @@ -88,7 +88,7 @@ Weitere Interface-Dateien im Ordner (z. B. Voice, Tickets, Messaging, Bootstrap) | `gateway/modules/serviceCenter/registry.py` | Service-Registry (CORE / IMPORTABLE) | | `gateway/modules/serviceCenter/resolver.py` | Auflösung von Service-Instanzen inkl. Cache | | `gateway/modules/serviceHub/__init__.py` | Hub / DI, `PublicService`-Wrapper für kontrollierte Oberflächen | -| `gateway/modules/routes/*.py` | HTTP-Endpunkte, Aufruf in Richtung Service Center / Features | +| `gateway/modules/routes/*.py` | HTTP-Endpunkte, Aufruf in Richtung Service Center / Features (inkl. `routeI18n.py` für DB-backed i18n mit AI-Übersetzung) | | `gateway/modules/interfaces/*.py` | Stabile Verträge und DB-Zugriffe (keine direkte Vendor-Logik in Services) | | `gateway/modules/connectors/*.py` | Vendor-spezifische Adapter (Auth, Transport, Mapping) | | `gateway/modules/security/*` | RBAC-Auswertung, RBAC-Katalog, Root-Access | diff --git a/c-work/1-plan/2026-04-analysis-editor.md b/c-work/1-plan/2026-04-analysis-editor.md deleted file mode 100644 index 0a126a7..0000000 --- a/c-work/1-plan/2026-04-analysis-editor.md +++ /dev/null @@ -1,398 +0,0 @@ - - - -# Analyse: Typed Node Handover System für den Graphical Editor - -## 1 Problemstellung - -Der Graphical Editor besitzt heute **keine formale Typisierung** der Daten, die zwischen Nodes fließen. Jede Node produziert ein freiformiges Dict; die Folge-Node versucht heuristisch, daraus das Passende zu extrahieren. Das führt zu: - -| Symptom | Wo sichtbar | -|---------|-------------| -| Spezialcode pro Paar (z.B. `_extractEmailContentFromUpstream`, `_getContextFromUpstream`, `_gatherAttachmentDocumentsFromUpstream`) | `actionNodeExecutor.py` (~860 Zeilen, >50 % handover Heuristik) | -| DataPicker zeigt **hartcodierte** Beispiel-Schemas (`outputPreviewRegistry.ts`) — stimmt oft nicht mit echten Laufzeit-Outputs überein | Frontend `outputPreviewRegistry.ts` | -| Kein `data.transform`, `data.filter`, `data.aggregate` implementiert — nur in `automation.md` als Placeholder aufgelistet | `nodeDefinitions/` hat keine Datei dafür | -| Kein „Aggregate" (Gegenstück zu `flow.loop`'s For-Each) | Engine sammelt Loop-Body-Resultate nicht auf | -| Frontend-Parameter haben **eigene** Typdefinitionen (node-level `type: "string"`) statt der in `MethodBase` etablierten `WorkflowActionParameter` mit `FrontendType`-Enum + Validierung | `ai.py`, `email.py` etc. vs. `methodBase.py` | - -**Kern:** Die Method/Action-Schicht hat bereits eine saubere Parameter-Typisierung (`WorkflowActionParameter`, `_validateType`, `_validateParameters`, `FrontendType`). Aber der Graph-Editor nutzt sie weder für Input-Konfiguration noch für die Handover-Logik. - ---- - -## 2 Ist-Zustand (Layer für Layer) - -### 2.1 Node-Definitionen (`nodeDefinitions/*.py`) - -Jede Node ist ein Dict mit: - -```python -{ - "id": "email.draftEmail", - "parameters": [ - {"name": "subject", "type": "string", "required": True, ...}, - ], - "inputs": 1, "outputs": 1, - "_method": "outlook", "_action": "composeAndDraftEmailWithContext", - "_paramMap": {"connectionId": "connectionReference", ...}, -} -``` - -**Was fehlt:** -- Kein `outputSchema` — was liefert die Node zurück? -- Kein `inputSchema` — was erwartet die Node am Eingang? -- `type: "string"` ist Node-Editor-intern, nicht identisch mit `WorkflowActionParameter.type` (`str`, `int`, `List[str]`, …). -- Kein `frontendType` (d.h. Frontend baut eigene Config-Components per Node-Typ statt generisch). - -### 2.2 Execution Engine (`executionEngine.py`) - -- Topologische Sortierung → iteriert Nodes → ruft Executor auf → speichert Output in `nodeOutputs[nodeId]`. -- **Kein Output-Vertrag**: der Executor liefert `Any`, das Dict wird 1:1 in `nodeOutputs` gelegt. -- Folge-Nodes holen via `inputSources[nodeId][0]` den Vorgänger-Output und _hoffen_, dass er das richtige Format hat. - -### 2.3 ActionNodeExecutor (`actionNodeExecutor.py`) - -- ~860 Zeilen, davon ~60 % dedizierte Merge-Logik für Paare: `email→AI`, `AI→email.draftEmail`, `file.create` content-gathering, ClickUp merge, etc. -- Aufruft am Ende `ActionExecutor.executeAction(method, action, params)` — dessen `MethodBase._validateParameters` prüft die **Action**-Parameter, nicht den Graph-Port-Kontrakt. -- Baut ein Output-Dict mit teilweise inkonsistenten Keys (`documents` vs. `documentList` vs. `data` vs. `context`). - -### 2.4 DataPicker + outputPreviewRegistry (Frontend) - -- `outputPreviewRegistry.ts` registriert **statische Beispiele** pro Node-Typ. -- DataPicker zeigt daraus einen Baum und erzeugt `DataRef { type: "ref", nodeId, path }`. -- `resolveParameterReferences` (Backend) löst `DataRef` gegen echte `nodeOutputs`. -- **Problem:** Die Preview-Struktur ist manuell gepflegt und **divergiert** vom echten Output. Der User wählt Pfade, die zur Laufzeit nicht existieren oder anders heißen. - -### 2.5 Method/Action Parameter-System (bereits existent) - -`WorkflowActionParameter` + `MethodBase._validateParameters`: - -```python -class WorkflowActionParameter(BaseModel): - name: str - type: str # 'str', 'int', 'List[str]', 'Dict[str, Any]' - frontendType: FrontendType # TEXT, TEXTAREA, SELECT, USER_CONNECTION, … - frontendOptions: Optional[...] - required: bool - default: Optional[Any] - validation: Optional[Dict] -``` - -`_validateType` konvertiert + prüft: `str→str`, `int→int`, `List[str]→[str,…]`, usw. - -**Dieses System funktioniert bereits** für Workspace-Actions. Es wird aber **nicht** für Node-Ein-/Ausgänge genutzt. - ---- - -## 3 Ziel-Architektur: Typed Port System - -### 3.1 Kern-Konzept: Port-Schema - -Jede Node deklariert **typisierte Ports** für Ein- und Ausgänge — analog zu `WorkflowActionParameter`, aber für den Graphen. - -``` -┌──────────────────────────────────────────────────────────────────────┐ -│ Node Definition (Beispiel: email.draftEmail) │ -│ │ -│ inputPorts: │ -│ [0]: { schema: EmailDraft } ← structured type │ -│ EmailDraft = { subject: str, body: str, to: List[str] } │ -│ │ -│ outputPorts: │ -│ [0]: { schema: ActionResult } │ -│ ActionResult = { success: bool, error: str?, │ -│ documents: List[Document] } │ -│ │ -│ parameters: (config, unabhängig von Ports) │ -│ connectionId: { type: str, frontendType: USER_CONNECTION } │ -└──────────────────────────────────────────────────────────────────────┘ -``` - -### 3.2 Port-Schema Definition - -```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): - fields: List[PortField] - -class NodePortDefinition(BaseModel): - inputPorts: Dict[int, PortSchema] # port-index → schema - outputPorts: Dict[int, PortSchema] # port-index → schema -``` - -### 3.3 Einheitliche Output-Typen (Port-Typen-Katalog) - -Statt freier Dicts gibt es benannte Schemas, die Node-übergreifend wiederverwendbar sind: - -| 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?: DocumentList` | ai.prompt (mode=email) | email.draftEmail, email.sendEmail | -| `EmailList` | `emails: List[{subject, from, to, body, date, attachments}]` | email.checkEmail, email.searchEmail | ai.prompt, flow.loop, flow.ifElse | -| `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, Keys = Feldnamen) | trigger.form, input.form | Alle (via Referenz auf Einzelfelder) | -| `AiResult` | `prompt: str, response: str, 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** (NEU) | Alle | - -### 3.4 Schema auf Node-Definitions - -Erweiterung der bestehenden Node-Definitions um `outputPorts` und `inputPorts`: - -```python -{ - "id": "ai.prompt", - "category": "ai", - "parameters": [...], # Config — bleibt wie bisher - "inputs": 1, - "outputs": 1, - - # NEU ───────────────────────────────────────── - "inputPorts": { - 0: {"accepts": ["DocumentList", "TextResult", "FormPayload", "EmailList", "AiResult", "Any"]}, - }, - "outputPorts": { - 0: {"schema": "AiResult"}, - }, -} -``` - -```python -{ - "id": "email.draftEmail", - "inputPorts": { - 0: {"accepts": ["EmailDraft", "AiResult", "TextResult"]}, - }, - "outputPorts": { - 0: {"schema": "ActionResult"}, - }, -} -``` - -### 3.5 Konsequenzen für bestehende Schichten - -#### A) Node-Definitionen — einmalige Migration - -Jede Node bekommt `inputPorts` + `outputPorts`. Bestehende `inputs`/`outputs` (Zählwerte) bleiben für Rückwärtskompatibilität, aber die Schemas werden die Quelle der Wahrheit. - -#### B) Executors — Output normalisieren - -Jeder Executor erhält eine `_normalizeOutput(rawResult, portSchema) → Dict` Funktion. Sie sorgt dafür, dass die Keys/Typen dem deklarierten Schema entsprechen. Für `AiResult` z.B.: - -```python -def _normalizeAiResult(raw: Any) -> Dict: - return { - "prompt": raw.get("prompt", ""), - "response": raw.get("response", raw.get("context", "")), - "context": raw.get("context", ""), - "documents": _ensureDocumentList(raw.get("documents", [])), - } -``` - -Der Spezialcode in `actionNodeExecutor.py` (~400 Zeilen Paar-Heuristik) fällt dann weg und wird durch generische Normalizer pro Port-Typ ersetzt. - -#### C) ExecutionEngine — Handover-Validierung - -Nach jedem Node-Execute: `nodeOutputs[nodeId]` wird gegen `outputPorts[0].schema` geprüft (Warnung oder Fehler bei Mismatch). Vor jedem Node-Execute: Input-Port-Kompatibilitäts-Check (`accepts` enthält den Schema-Typ des Vorgänger-Outputs). - -#### D) Frontend — DataPicker aus Schema generieren - -`outputPreviewRegistry.ts` wird ersetzt durch eine generische Funktion, die aus dem PortSchema den Preview-Baum baut. Kein manuelles Pflegen mehr: - -```typescript -function buildPreviewFromSchema(schema: PortSchema): Record { - // Iteriert schema.fields → Beispielwert pro Typ -} -``` - -#### E) Frontend — Generisches Parameter-Rendering - -Statt pro Node-Typ eine eigene Config-Component zu schreiben (aktuell 12+ Dateien), können die meisten Parameter-Felder generisch aus dem `WorkflowActionParameter`-Schema gerendert werden. Spezial-UIs (FormBuilder, ClickUp Browse, etc.) bleiben als Override. - -#### F) Verbindungs-Validierung im Editor - -Beim Ziehen einer Kante: prüfe `sourceNode.outputPorts[outputIdx].schema` ∈ `targetNode.inputPorts[inputIdx].accepts`. Falls inkompatibel: Kante rot markieren / verhindern. - ---- - -## 4 Fehlende Nodes / Konzepte - -### 4.1 data.aggregate (Gegenstück zu flow.loop) - -Loop verteilt Items → Body → aber am Ende gibt es keinen Collector. Heute liegt nur das letzte Body-Ergebnis in `nodeOutputs`. - -**Lösung: `data.aggregate`-Node** - -```python -{ - "id": "data.aggregate", - "category": "data", - "label": {"en": "Aggregate", "de": "Sammeln", "fr": "Agréger"}, - "description": {"en": "Collect results from loop body into a list", ...}, - "parameters": [ - {"name": "mode", "type": "string", "options": ["collect", "concat", "sum", "count"], "default": "collect"}, - ], - "inputs": 1, "outputs": 1, - "inputPorts": {0: {"accepts": ["Any"]}}, - "outputPorts": {0: {"schema": "AggregateResult"}}, - "executor": "data", -} -``` - -**Engine-Änderung:** Im Loop-Body erkennt die Engine eine `data.aggregate`-Node und sammelt pro Iteration das Ergebnis in einer Liste. Nach Loop-Ende: `nodeOutputs[aggregateNodeId] = { items: [...], count: N }`. - -### 4.2 data.transform - -Reiner Mapping-Node: Felder umbenennen, extrahieren, umstrukturieren. Konfiguration per Key-Value-Mapping oder einfachem Expression-Sprache. - -```python -{ - "id": "data.transform", - "category": "data", - "parameters": [ - {"name": "mapping", "type": "json", "description": "Key-Value Mapping: {outputField: inputRef}"}, - ], - "inputPorts": {0: {"accepts": ["Any"]}}, - "outputPorts": {0: {"schema": "Dict"}}, -} -``` - -### 4.3 data.filter - -Filtert Items einer Liste nach Bedingung (wie WHERE in SQL). Eingabe: List, Ausgabe: gefilterter List. - -```python -{ - "id": "data.filter", - "category": "data", - "parameters": [ - {"name": "condition", "type": "string", "description": "Filter expression (z.B. item.status == 'open')"}, - ], - "inputPorts": {0: {"accepts": ["AggregateResult", "FileList", "TaskList", "EmailList"]}}, - "outputPorts": {0: {"schema": "AggregateResult"}}, -} -``` - -### 4.4 flow.merge - -Heute in `automation.md` gelistet, aber nicht implementiert. Wartet auf N Eingänge und kombiniert sie. - ---- - -## 5 Migrations-Strategie - -### Phase 1: Port-Schema-Deklaration (Backend, kein Breaking Change) - -1. `PortSchema` als Pydantic-Modell definieren. -2. Katalog der Port-Typen (`DocumentList`, `AiResult`, `EmailDraft`, …) anlegen. -3. Jede Node-Definition in `nodeDefinitions/*.py` um `inputPorts` / `outputPorts` erweitern. -4. API `GET /node-types` liefert die Schemas mit. - -### Phase 2: Output-Normalizer (Backend) - -1. Pro Port-Typ einen Normalizer schreiben (10–15 Funktionen). -2. In `executionEngine.py`: nach jedem Execute `_normalizeOutput(result, schema)` aufrufen. -3. Spezialcode in `actionNodeExecutor.py` schrittweise durch Normalizer ersetzen. -4. Step-Logs mit normalisiertem Output (Debugging/Tracing sofort besser). - -### Phase 3: Frontend — DataPicker aus Schema (Frontend) - -1. `outputPreviewRegistry.ts` durch Schema-basierte Preview-Generierung ersetzen. -2. DataPicker zeigt korrekte Felder + Typen an. -3. Verbindungs-Validierung (rote Kanten bei Typ-Mismatch). - -### Phase 4: Generisches Parameter-Rendering (Frontend) - -1. `NodeConfigPanel` rendert Parameter generisch aus `WorkflowActionParameter`-Schema. -2. Spezial-Components (`FormNodeConfig`, `ClickUpNodeConfig`, …) bleiben als Override für komplexe UIs. -3. Neue Nodes brauchen **keine eigene Config-Component** mehr. - -### Phase 5: Neue Data-Nodes - -1. `data.aggregate` implementieren (Engine + Node-Definition + Frontend). -2. `data.transform` implementieren. -3. `data.filter` implementieren. -4. `flow.merge` implementieren. - ---- - -## 6 Konkretes Beispiel: AI-Mail-Entwurf → Mail-Versand - -### Heute (heuristisch) - -``` -[email.checkEmail] ──→ [ai.prompt] ──→ [email.draftEmail] - ↓ ↓ ↓ - freeform Dict freeform Dict ~100 Zeilen Spezialcode - (data.emails…) (context, docs) in actionNodeExecutor: - _extractEmailContentFromUpstream - _getIncomingEmailFromUpstream - _unpackIncomingEmail - _gatherAttachmentDocumentsFromUpstream -``` - -### Ziel (typisiert) - -``` -[email.checkEmail] [ai.prompt] [email.draftEmail] - outputPort[0]: outputPort[0]: inputPort[0]: - schema: EmailList schema: AiResult accepts: [EmailDraft, AiResult] - - ──────────────────→ ──────────────────→ Normalizer kennt AiResult und - Input: EmailList Input: EmailList mapped response → body, - Engine prüft Kompatibilität Engine prüft kein Spezialcode nötig. -``` - -**Entscheidend:** Der AI-Node kann so konfiguriert werden, dass sein Output-Typ `EmailDraft` statt `AiResult` ist (z.B. via Parameter `outputMode: "emailDraft"` oder via Prompt-Instruktion + structured output). Dann ist der Downstream-Merge trivial. - ---- - -## 7 Offene Fragen / Entscheidungen - -| # | Frage | Optionen | -|---|-------|----------| -| 1 | Soll Typ-Mismatch eine Kante verhindern (hard) oder nur warnen (soft)? | **Empfehlung: soft** (Warnung + gelbe Kante). Hard-Block wäre zu restriktiv bei generischen Nodes. | -| 2 | Port-Typ `Any` erlauben? | Ja, als Fallback. Nodes wie `flow.ifElse` leiten den Input transparent durch. | -| 3 | Dynamische Schemas (z.B. `input.form` Payload hängt von Felddefinition ab)? | Ja. `outputPorts` kann eine Funktion referenzieren, die das Schema aus den `parameters` ableitet (wie heute `outputPreviewRegistry`). | -| 4 | Sollen bestehende Workflows beim Upgrade automatisch migriert werden? | **Nein.** Alte Workflows laufen weiter (Normalizer fängt fehlende Keys ab). Neue Workflows profitieren von Validierung. | -| 5 | Separater `data.aggregate`-Node oder implizit in `flow.loop`? | **Empfehlung: separater Node.** Explizit ist klarer; der Loop-Node bleibt rein für Iteration. | -| 6 | Braucht es einen `data.join`-Node (merge zwei Listen)? | Später. Erst aggregate + filter + transform etablieren. | - ---- - -## 8 Aufwand-Schätzung - -| Phase | Geschätzter Aufwand | Abhängigkeiten | -|-------|-------------------|----------------| -| 1 — Port-Schema-Deklaration | 2–3 Tage | Keine | -| 2 — Output-Normalizer | 3–5 Tage | Phase 1 | -| 3 — Frontend DataPicker aus Schema | 2–3 Tage | Phase 1 | -| 4 — Generisches Parameter-Rendering | 3–4 Tage | Phase 1 | -| 5 — Data Nodes (aggregate, transform, filter) | 3–5 Tage | Phase 2 | -| **Gesamt** | **~13–20 Tage** | Phasen 1–3 sind der kritische Pfad | - ---- - -## 9 Schlüssel-Dateien - -| Bereich | Pfade | -|---------|-------| -| Node-Definitionen | `gateway/modules/features/graphicalEditor/nodeDefinitions/*.py` | -| Execution Engine | `gateway/modules/workflows/automation2/executionEngine.py` | -| ActionNodeExecutor (Handover-Heuristik) | `gateway/modules/workflows/automation2/executors/actionNodeExecutor.py` | -| Graph-Utilities | `gateway/modules/workflows/automation2/graphUtils.py` | -| Method/Action Typsystem | `gateway/modules/workflows/methods/methodBase.py`, `gateway/modules/datamodels/datamodelWorkflowActions.py` | -| FrontendType Enum | `gateway/modules/shared/frontendTypes.py` | -| Output-Preview (Frontend) | `frontend_nyla/src/components/FlowEditor/nodes/shared/outputPreviewRegistry.ts` | -| DataPicker (Frontend) | `frontend_nyla/src/components/FlowEditor/nodes/shared/DataPicker.tsx` | -| DataRef/DynamicValue (Frontend) | `frontend_nyla/src/components/FlowEditor/nodes/shared/dataRef.ts` | -| Node Config Registry (Frontend) | `frontend_nyla/src/components/FlowEditor/nodes/configs/index.ts` | -| DataFlow Context (Frontend) | `frontend_nyla/src/components/FlowEditor/context/Automation2DataFlowContext.tsx` | diff --git a/c-work/1-plan/2026-04-gateway-int-stability-and-bugfixes.md b/c-work/1-plan/2026-04-gateway-int-stability-and-bugfixes.md new file mode 100644 index 0000000..417b4a0 --- /dev/null +++ b/c-work/1-plan/2026-04-gateway-int-stability-and-bugfixes.md @@ -0,0 +1,123 @@ + + + + +# Gateway: INT-Stabilität — Analyse (Log) und Fix-Plan + +## Beschreibung und Kontext + +Beim Testen der Applikation auf der **Integrations-Instanz (INT)** mit **mehreren parallelen Nutzern** wurden Symptome im Gateway-Log sichtbar (u. a. `gateway_log_2026-04-08T11-14-17-514Z.log`). Ziel dieses Dokuments ist eine **codebezogene Ursachenanalyse** und ein **Umsetzungs- und Testplan** — ohne die Themen mit Architektur-Storys zu vermischen. + +**Business-Treiber:** Zuverlässiger Multi-User-Betrieb (Upload/Index, Workspace-Agent, Outlook-Tools, Knowledge/Neutralisierung) und weniger „sporadische“ Fehler, die Support-Zeit kosten. + +**Risiko bei Nicht-Umsetzung:** Weiterhin nicht reproduzierbar wirkende Permission-Fehler nach erfolgreicher Verarbeitung, fehlgeschlagene OpenAI-Streaming-Calls wegen Tool-Schema, nutzloser Outlook-Tool-Flow bei UUID-Referenzen, Crashes/Fail-Safe-Sprünge bei Bild-only-Indexierung. + +**Abhängigkeiten:** Änderungen betreffen primär `gateway`; keine zwingende Frontend-Änderung, ausser Tool-Beschreibungen/UX explizit angepasst werden. + +## Fokus und kritische Details + +### Thema A — Management-Interface: globaler Singleton + +- **Fragil:** `interfaceDbManagement.getInterface()` liefert eine **gemeinsame** `ComponentObjects`-Instanz (`"default"`); jeder Aufruf setzt `setUserContext` auf demselben Objekt. +- **Edge Case:** Async-Pfade (z. B. `_autoIndexFile` mit `await knowledgeService.indexFile`) — zwischen Start und `updateFile` kann ein anderer Request den Kontext überschreiben → `PermissionError` auf `FileItem.update` trotz erfolgreicher Indexierung. +- **Code:** `gateway/modules/interfaces/interfaceDbManagement.py`, `gateway/modules/routes/routeDataFiles.py` (`_autoIndexFile`). + +### Thema B — Agent-Tool-JSON-Schema: Arrays ohne `items` + +- **Fragil:** `ActionToolAdapter` mappt `List[str]` auf JSON-Schema `type: array` **ohne** `items`. +- **Edge Case:** OpenAI lehnt Tools mit HTTP 400 ab (*array schema missing items*); Failover zu anderen Providern maskiert das Problem. +- **Code:** `gateway/modules/serviceCenter/services/serviceAgent/actionToolAdapter.py`; betroffene Aktion u. a. `ai.process` (`methodAi.py`, Parameter `documentList`). + +### Thema C — Outlook: Referenzformat vs. Lookup + +- **Fragil:** `listConnections` zeigt u. a. `id: `; `getUserConnectionFromConnectionReference` parst nur `connection:{authority}:{username}`. +- **Edge Case:** Modell übergibt UUID an Outlook-Tools → „Connection not found“. +- **Code:** `coreTools/_connectionTools.py`, `mainServiceChat.getUserConnectionFromConnectionReference`, `methodOutlook/helpers/connection.py`. + +### Thema D — Knowledge: `_neutralSvc` bei nur Bildern + +- **Bug:** `_neutralSvc` wird nur im Zweig mit Textobjekten gesetzt; Bild-Neutralisierung nutzt `_neutralSvc` danach — bei **keinen** Textobjekten → `UnboundLocalError` (im Log als fail-safe sichtbar). +- **Code:** `gateway/modules/serviceCenter/services/serviceKnowledge/mainServiceKnowledge.py` (`indexFile`). + +### Thema E — Workspace: Mandats-Kontext aus zwei Quellen + +- **Fragil:** `_getChatInterface` nutzt `context.mandateId`; `ServiceCenterContext` für den Agent nutzt Mandat aus `_validateInstanceAccess` (Instanz). +- **Edge Case:** Unterschiedliche `interfaceDbChat`-Cache-Keys für „denselben“ Workspace → seltene Inkonsistenzen (Workflow sichtbar / nicht sichtbar). +- **Code:** `gateway/modules/features/workspace/routeFeatureWorkspace.py`. + +### Thema F — Chat: `createMessage` / `getWorkflow` → None + +- **Fragil:** `getWorkflow` liefert `None` bei leerem RBAC-Recordset **und** bei Exceptions beim Validieren des `ChatWorkflow`-Modells; `createMessage` meldet pauschal „No access to workflow“. +- **Edge Case:** Agent-Lauf endet „complete“, Persistenz der Assistant-Message schlägt fehl — Ursache ohne besseres Logging schwer trennbar. +- **Code:** `gateway/modules/interfaces/interfaceDbChat.py`. + +## Ziel und Nicht-Ziele + +- **Ziel:** Die oben genannten **konkreten** Defekte/Risiken beheben oder absichern; INT- und Last-Szenarien reproduzierbar testen; Logging dort verbessern, wo die Ursache heute verschleiert wird. +- **Explizit NICHT:** Grossrefactor des gesamten Service-Centers; neue Features; umfassende Wiki-b-reference Pflege (erst nach Abschluss laut README-Lebenszyklus). + +## Betroffene Module + +- **Gateway:** `interfaceDbManagement`, `routeDataFiles`, `actionToolAdapter`, `mainServiceChat`, `mainServiceKnowledge`, `routeFeatureWorkspace`, `interfaceDbChat`, Agent-Core-Tools (`_connectionTools`), optional `connection.py` (Outlook). +- **Frontend:** nein (ausser spätere Texte/Hinweise für Connection-Referenz — optional). +- **DB-Migration:** nein (für diese Fixes nicht erwartet). +- **Andere Komponenten:** nein. + +## Entscheidungen + +| Datum | Entscheidung | Begründung | +|-------|-------------|------------| +| 2026-04-08 | Plan-Dokument angelegt | INT-Log + Code-Review; Umsetzung folgt in `2-build`. | + +## Umsetzungs-Checkliste (Fix-Reihenfolge empfohlen) + +- [ ] **D** `_neutralSvc` / Bildpfad in `mainServiceKnowledge.indexFile` absichern (klein, klar). +- [ ] **B** JSON-Schema für `List[str]` / `List[int]` (und ggf. weitere Array-Typen) in `actionToolAdapter` ergänzen. +- [ ] **C** Connection-Lookup UUID **oder** einheitliche Ausgabe/Beschreibung in `listConnections` + Parser in `getUserConnectionFromConnectionReference`. +- [ ] **A** Management-Interface: Request-scoped Instanz statt globalem Singleton **oder** dokumentierter alternativer Ansatz (kein geteilter mutable User-Kontext über `await`-Grenzen). +- [ ] **E** Workspace: einheitliche `mandateId`-Quelle für Chat-Interface und `ServiceCenterContext` nach Instanz-Validierung. +- [ ] **F** `getWorkflow` / `createMessage`: präziseres Logging bei Validierungsfehlern; ggf. Unterscheidung „nicht gefunden“ vs. „Daten ungültig“. + +Weitere Checkliste aus Template: + +- [ ] API-Endpunkte (nur falls Signatur/Kontrakt ändert) +- [ ] DB-Schema / Migration — nein +- [ ] Frontend-Komponenten — nein (optional) +- [ ] RBAC / Permissions — prüfen nach Fix A/E +- [ ] Neutralisierung betroffen? — ja (Thema D) +- [ ] Navigation / Routing — nein +- [ ] Billing-Impact? — nein (keine bewusste Änderung der Abrechnungslogik) + +## Akzeptanzkriterien + +| # | Kriterium (Given-When-Then) | Prio | +|---|---------------------------|------| +| AC1 | Given eine Datei mit nur Bild-`contentObjects` und aktiver Neutralisierung, When `indexFile` läuft, Then kein `UnboundLocalError` und definiertes Verhalten (OK/skip mit Log). | must | +| AC2 | Given generiertes Tool-Schema für `ai_process`, When es an die OpenAI-kompatible API geht, Then `documentList` ist ein `array` **mit** `items` (kein 400 wegen fehlender `items`). | must | +| AC3 | Given `listConnections` liefert eine Verbindungs-UUID, When ein Outlook-Tool dieselbe Referenz nutzt, Then Verbindung wird aufgelöst **oder** die Tool-Konvention verbietet UUID explizit und der Agent folgt zuverlässig der Konvention. | must | +| AC4 | Given zwei parallele Uploads/Auto-Index-Läufe unterschiedlicher User, When Indexierung endet, Then `updateFile(..., active)` schlägt nicht wegen **fremdem** User-Kontext fehl. | must | +| AC5 | Given Workspace-Stream mit gültiger Instanz, When Chat-Interface und Agent-Chat-Service erzeugt werden, Then identische effektive `mandateId`/`featureInstanceId`-Kombination für den Chat-Cache. | should | +| AC6 | Given `getWorkflow` scheitert an Datenvalidierung, When `createMessage` läuft, Then Log enthält **ursächliche** Exception, nicht nur „No access“. | should | + +## Testplan + +| ID | AC | Art | Automatisiert | Repo-Pfad / Methode | Status | +|----|----|-----|--------------|---------------------|--------| +| T1 | AC1 | unit | ja | `gateway/tests/...` — `indexFile` mit Mock neutralization, nur image `contentObjects` | pending | +| T2 | AC2 | unit | ja | Test: `_buildToolDefinition` / Schema für `ai_process` → assert `properties.documentList.items` | pending | +| T3 | AC3 | unit/integration | ja/teils | `getUserConnectionFromConnectionReference(uuid)` vs. `connection:msft:user@…` | pending | +| T4 | AC4 | integration | empfohlen | Zwei parallele Requests gegen INT oder lokaler Stress-Skript + assert File-Status | pending | +| T5 | AC5 | integration | optional | Request ohne Header-Mandat, mit Instanz-ID — ein Log-Punkt für effektive Chat-Keys | pending | +| T6 | AC6 | manuell/log | teils | Nach Fix: künstlich kaputtes Workflow-Datum in DB oder temporärer Log-Level | pending | + +## Links + +- PR: (nach Umsetzung) +- Issue: (falls extern getrackt) +- Ausgangs-Log (lokal): `local/debug/gateway_log_2026-04-08T11-14-17-514Z.log` + +## Abschluss + +- [ ] b-reference/ aktualisiert — z. B. `b-reference/gateway/architecture.md` nur wenn Architektur-Entscheid (Singleton) dauerhaft geändert wird +- [ ] TOPICS.md aktualisiert (falls neues Thema) +- [ ] Dieses Dokument bei Umsetzungsbeginn nach `c-work/2-build/` verschieben; nach Release nach `4-done/` bzw. `z-archive/` diff --git a/c-work/2-build/2026-04-generic-graph-editor.md b/c-work/2-build/2026-04-generic-graph-editor.md new file mode 100644 index 0000000..e9f9369 --- /dev/null +++ b/c-work/2-build/2026-04-generic-graph-editor.md @@ -0,0 +1,694 @@ + + + +# 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": , +} +``` + +| 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> = { + 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 ; + }); +} +``` + +--- + +## 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 16–63). + +**Ä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 54–75): + +- `_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 78–89): bleibt, `_method`/`_action` weiterhin verwendet. + +### 7.5 ÄNDERN: `gateway/modules/workflows/automation2/graphUtils.py` + +**Funktion `resolveParameterReferences`** (Zeile 185–243): + +- 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 86–120): 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 55–65): 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 199–207): analog Transit-Envelope. + +**Funktion `_loop`** (Zeile 261–272): 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 71–85): Case für `data.*` hinzufügen → `DataExecutor`. + +**Funktion `_is_node_on_active_path`** (Zeile 39–68): Transit-Envelope berücksichtigen — `branch`/`match` jetzt in `out["_meta"]` statt direkt in `out`. + +**Loop-Body-Verarbeitung** (Zeile 400–460): 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 146–168): 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 14–20): erweitern um `frontendType`, `frontendOptions`, `options`, `validation`. + +**Interface `NodeType`** (Zeile 22–39): erweitern um `inputPorts`, `outputPorts`. + +**Interface `NodeTypesResponse`** (Zeile 46–49): 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 ; +}); +``` + +### 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 +): Record { + 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 20–40): **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 10–19): 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 | diff --git a/c-work/4-done/2026-04-ui-i18n-dynamic-language-sets.md b/c-work/4-done/2026-04-ui-i18n-dynamic-language-sets.md new file mode 100644 index 0000000..bd4c42d --- /dev/null +++ b/c-work/4-done/2026-04-ui-i18n-dynamic-language-sets.md @@ -0,0 +1,195 @@ + + + + + +# UI-Mehrsprachigkeit: Dynamische Sprachsets (DB-backed i18n) + +## Beschreibung und Kontext + +Das UI nutzt ein **eigenes i18n-System** (`LanguageContext`, `t()`-Hook, `useLanguage`). Sprachsets werden dynamisch aus der Datenbank via public API geladen — keine statischen TypeScript-Dateien mehr. + +**Business-Treiber:** Multi-Tenant-Plattform mit Kunden in CH/DE/AT und perspektivisch FR/EN — neue Sprachen ohne Code-Änderung + Redeploy; Kunden können selbst neue Sprachen anlegen (AI-generiert). + +**ISO-Code Deutsch:** `de` (ISO 639-1 Standard). + +## Ergebnis (Ist-Zustand nach Umsetzung) + +| Aspekt | Umgesetzt | +|--------|-----------| +| Sprachen | Dynamischer `string`-Code, kein hardcoded Union-Type | +| Quelle | DB via public API (`GET /api/i18n/sets/{code}`) | +| Neue Sprache anlegen | AI-generiert, async, User-ausgelöst via Admin-UI | +| `t()` Coverage | Bestehende Dateien migriert; **jede neue Code-Anpassung muss UI-Texte mit `t()` erfassen** | +| Key-Schema | **Deutscher Text = Key** (kein Dot-Notation-Schema) | +| Variable Interpolation | Nativ in `t()`: `t('Text mit {variable}', {variable: 'Wert'})` | +| Sprachset-Verwaltung | CRUD-API: get codes, add, get, update, delete, download, update-all, export, import | +| Admin-UI | Administration → System → UI-Sprachen (`/admin/languages`) inkl. Export/Import | +| AI-Übersetzung | Batch-Pipeline via `AiObjects.callWithTextContext` + Billing | +| Statische Locale-Files | Entfernt (`de.ts`, `en.ts`, `fr.ts` gelöscht); Seed-Daten in DB | + +## Key-Konvention: Deutscher Text = Key + +**Grundprinzip:** Der deutsche Klartext IST der Key. Das `de`-Set ist trivial (Key = Value). Alle anderen Sets mappen denselben Key auf die jeweilige Übersetzung. + +```tsx +t('Abbrechen') // de: "Abbrechen", en: "Cancel", fr: "Annuler" +t('Speichern') // de: "Speichern", en: "Save", fr: "Enregistrer" +t('{authority} Verbindung bearbeiten', {authority: 'Google'}) + // de: "Google Verbindung bearbeiten" + // en: "Edit Google connection" +``` + +### DB-Struktur + +``` +de-Set: { "Abbrechen": "Abbrechen", "Speichern": "Speichern", ... } +en-Set: { "Abbrechen": "Cancel", "Speichern": "Save", ... } +fr-Set: { "Abbrechen": "Annuler", "Speichern": "Enregistrer", ... } +``` + +## Entwickler-Pflicht: t()-Tagging bei jeder Code-Änderung + +> **Regel:** Jeder neue oder geänderte UI-Text (Label, Button, Placeholder, Tooltip, Fehlermeldung) MUSS mit `t('Deutscher Klartext')` getaggt werden. Hardcodierte deutsche Strings im JSX sind nicht erlaubt. + +### Workflow für Entwickler + +1. **Neuer Text:** `t('Mein neuer Text')` verwenden — der Key wird automatisch Teil des `de`-Masters +2. **Text ändern:** `t('Alter Text')` → `t('Neuer Text')` — der alte Key verwaist, der neue fehlt in anderen Sets +3. **Variable Interpolation:** `t('{count} Einträge gefunden', { count: String(total) })` — Platzhalter `{...}` werden ersetzt +4. **Kein Plural-Framework:** Separate Keys verwenden, z.B. `t('1 Eintrag')` vs. `t('{count} Einträge', { count })` +5. **Import:** `const { t } = useLanguage();` — in jeder Komponente die `t()` nutzt +6. **Sync:** Admin klickt "Update All" in Administration → System → UI-Sprachen → System scannt automatisch die Codebase, synchronisiert das `de`-Master-Set (neue Keys rein, verwaiste raus), dann AI übersetzt fehlende Keys in allen anderen Sets + +### Sonderfall: gleicher Text, anderer Kontext + +Falls nötig (z.B. "Offen" = "Open" vs. "Outstanding"): `t('Offen (Status)')` vs. `t('Offen (Zustand)')`. Die Klammer ist Teil des Keys und dient AI als Kontext-Hinweis. + +## Architektur-Übersicht + +### Backend (Gateway) + +| Datei | Zweck | +|-------|-------| +| `modules/datamodels/datamodelUiLanguage.py` | Pydantic-Model `UiLanguageSet` (id, label, keys, status, isDefault) | +| `modules/routes/routeI18n.py` | API-Routen: public GET, auth POST, SysAdmin PUT/DELETE | +| `modules/interfaces/interfaceDbManagement.py` | `_seedUiLanguageSetsIfEmpty()` — initiales DB-Seeding | +| `modules/migration/seedData/ui_language_seed.json` | Seed-Daten für `de`, `en`, `fr` | +| `modules/system/mainSystem.py` | Navigationseintrag `admin-languages` | +| `scripts/build_ui_language_seed_json.py` | Script zur Seed-JSON-Generierung | +| `scripts/i18n_rekey_plaintext_keys.py` | Script zur Migration Dot-Notation → Klartext-Keys | + +### Frontend (Nyla) + +| Datei | Zweck | +|-------|-------| +| `src/locales/index.ts` | API-basiertes Language-Loading (kein static-import) | +| `src/locales/types.ts` | `Language = string` (dynamisch) | +| `src/providers/language/LanguageContext.tsx` | `t()` mit Interpolation, Fallback-Kette, `availableLanguages` | +| `src/pages/admin/AdminLanguagesPage.tsx` | Admin-Seite mit FormGeneratorTable | +| `src/config/pageRegistry.tsx` | Icon-Mapping `page.admin.languages` | + +### API-Endpunkte + +| Methode | Pfad | Auth | Zweck | +|---------|------|------|-------| +| GET | `/api/i18n/codes` | public | Liste aller Sprachcodes + Status | +| GET | `/api/i18n/sets/{code}` | public | Sprachset laden | +| GET | `/api/i18n/sets/{code}/download` | auth | JSON-Download | +| POST | `/api/i18n/sets` | auth + billing | Neue Sprache anlegen (async AI) | +| PUT | `/api/i18n/sets/sync-de` | SysAdmin | `de`-Master aus Codebase synchronisieren (t()-Scan) | +| PUT | `/api/i18n/sets/{code}` | SysAdmin | de-Sync + Set synchronisieren (AI für fehlende Keys) | +| PUT | `/api/i18n/sets/update-all` | SysAdmin | de-Sync + alle Non-`de`-Sets synchronisieren | +| DELETE | `/api/i18n/sets/{code}` | SysAdmin | Set löschen (nicht `de`) | +| GET | `/api/i18n/export` | SysAdmin | Komplette Sprachdatenbank als JSON exportieren | +| POST | `/api/i18n/import` | SysAdmin | JSON-Datei importieren (upsert, kein Löschen) | + +### AI-Pipeline + +- **Create:** Background-Job übersetzt alle ~928 Keys in Batches à 80 via `AiObjects.callWithTextContext` +- **Update:** Synchron — nur fehlende Keys werden per AI übersetzt, überzählige entfernt +- **Billing:** Jeder AI-Call wird via `BillingService.recordUsage` abgerechnet (Mandats-Pool des auslösenden Users) +- **Fallback:** Bei AI-Fehler wird `[Deutscher Klartext]` als Platzhalter gesetzt (eckige Klammern = erkennbar unübersetzt), Status `incomplete` +- **de-Master-Sync:** Vor jedem Update/Update-All wird automatisch `_syncDeMasterFromCodebase()` ausgeführt — scannt alle `t()`-Aufrufe im Frontend, fügt neue Keys hinzu, entfernt verwaiste + +### de-Master-Sync aus Codebase + +Bei **Update**, **Update All** und dem dedizierten Endpunkt `PUT /api/i18n/sets/sync-de` wird automatisch: + +1. Alle `.ts`/`.tsx`-Dateien unter `frontend_nyla/src/` nach `t('...')`-Aufrufen gescannt +2. Neue Keys (in Codebase, nicht in DB) → zum `de`-Set hinzugefügt (Key = Value = deutscher Klartext) +3. Verwaiste Keys (in DB, nicht mehr in Codebase) → aus dem `de`-Set entfernt +4. Danach erst werden die Non-`de`-Sets synchronisiert (fehlende Keys per AI übersetzen, überzählige entfernen) + +### t()-Funktion Fallback-Kette + +1. Ziel-Sprachset (z.B. `en`) +2. `de`-Master-Set (immer geladen) +3. Zweites Argument als String-Fallback (falls übergeben) +4. **`[Key]`** — eckige Klammern markieren den Key als unübersetzt/fehlend, damit er im UI sofort erkennbar ist + +## Entscheidungen + +| Datum | Entscheidung | Begründung | +|-------|-------------|------------| +| 2026-04-08 | ISO-Code `de` für Deutsch | ISO 639-1 Standard | +| 2026-04-08 | Eigenes `t()`-System beibehalten | 150+ Dateien integriert; i18next wäre Overhead | +| 2026-04-08 | **Deutscher Text = Key** | Selbst-dokumentierend, AI-Kontext eingebaut | +| 2026-04-08 | Keine Plural-Logik in `t()` | Separate Keys reichen | +| 2026-04-08 | Keine statischen Locale-Files | DB ist einzige Quelle; kein Fallback | +| 2026-04-08 | Key-Sync über Update-API | Dev taggt `t()`, Admin klickt "Update All" | +| 2026-04-08 | AI-Batch-Übersetzung mit Billing | `AiObjects` + `BillingService.recordUsage` | +| 2026-04-08 | de-Master-Sync aus Codebase | Automatischer t()-Scan vor jedem Update; kein manuelles Pflegen des de-Sets | +| 2026-04-08 | Fallback `[Key]` statt nackter Key | Eckige Klammern machen unübersetzte Texte im UI sofort sichtbar | +| 2026-04-08 | Export/Import der kompletten Sprachdatenbank | Instanz-übergreifender Transfer (INT → PROD) ohne DB-Zugriff | + +## Umsetzungs-Checkliste (abgeschlossen) + +### Phase 0 — t()-Tagging: Klartext-Keys + vollständige Coverage ✅ + +- [x] AI-Scan: Replacement-Liste erstellt (read-only) +- [x] Script `i18n_rekey_plaintext_keys.py`: Replacements mechanisch ausgeführt +- [x] Script `build_ui_language_seed_json.py`: `de`-Master-Set extrahiert +- [x] `en`/`fr`-Sets migriert (mechanisch, Key-Remapping) +- [x] Seed-Daten als `ui_language_seed.json` bereitgestellt +- [x] Statische Locale-Files `de.ts`, `en.ts`, `fr.ts` entfernt + +### Phase 1 — Gateway: Datamodel + API ✅ + +- [x] Datamodel `UiLanguageSet` (`datamodelUiLanguage.py`) +- [x] DB-Tabelle registriert (Auto-Deploy) +- [x] Routes `routeI18n.py` (7 Endpunkte) +- [x] `createUiLanguage`: Pre-flight Billing → Background-Job → AI-Batch-Übersetzung → Notification +- [x] `updateUiLanguage`: `de`-Master → AI-Übersetzung fehlender Keys → überzählige entfernt +- [x] Seed: `_seedUiLanguageSetsIfEmpty()` in `interfaceDbManagement.py` +- [x] AI-Pipeline: `AiObjects.callWithTextContext` + `BillingService.recordUsage`, Batch-Size 80 + +### Phase 2 — Frontend: LanguageContext + t() ✅ + +- [x] `Language`-Type → `string` +- [x] `loadLanguage` → API statt static-import +- [x] `t()` mit `{variable}`-Interpolation +- [x] Fallback-Kette: Ziel → `de` → Fallback-String → Key +- [x] `availableLanguages` + `refreshAvailableLanguages` +- [x] Sprach-Dropdown in Settings: dynamisch + +### Phase 3 — Admin-UI: Sprachverwaltung ✅ + +- [x] Admin-Seite `/admin/languages` (`AdminLanguagesPage.tsx`) +- [x] FormGeneratorTable: Code, Label, Status, Keys-Count +- [x] Row-Actions: Update, Delete (nicht `de`), Download +- [x] Toolbar-Actions: Add (Billing-Warning), Update All, Export, Import +- [x] Navigationseintrag in `mainSystem.py` + `pageRegistry.tsx` + +### Querschnitt ✅ + +- [x] RBAC: POST → auth; PUT/DELETE → SysAdmin; GET → public +- [x] Billing: AI-Credits via `BillingService.recordUsage` +- [x] Navigation: Admin-Route + Menüeintrag + +## Links + +- LanguageContext: `frontend_nyla/src/providers/language/LanguageContext.tsx` +- API-Route: `gateway/modules/routes/routeI18n.py` +- Admin-Seite: `frontend_nyla/src/pages/admin/AdminLanguagesPage.tsx` +- Seed-Daten: `gateway/modules/migration/seedData/ui_language_seed.json` diff --git a/d-guides/coding-conventions.md b/d-guides/coding-conventions.md index 585d2f2..6c1f3d5 100644 --- a/d-guides/coding-conventions.md +++ b/d-guides/coding-conventions.md @@ -1,4 +1,4 @@ - + doku nachgeführt werden an # Coding-Konventionen @@ -17,6 +17,30 @@ - Hooks-Pattern fuer State und API-Zugriffe (`useApiRequest`, `useBilling`, etc.) - Fehler propagieren -- keine stillen Fallbacks bei kritischen Pfaden +### i18n-Pflicht: `t()` fuer alle UI-Texte + +Jeder sichtbare Text im UI (Labels, Buttons, Placeholders, Tooltips, Fehlermeldungen) **muss** mit `t()` getaggt werden. Hardcodierte deutsche Strings im JSX sind nicht erlaubt. + +```tsx +import { useLanguage } from '../../providers/language/LanguageContext'; + +const { t } = useLanguage(); + +// Einfacher Text +t('Speichern') + +// Mit Variablen-Interpolation +t('{count} Eintraege gefunden', { count: String(total) }) + +// Gleicher Text, anderer Kontext → Klammer als Kontext-Hinweis +t('Offen (Status)') // vs. t('Offen (Zustand)') +``` + +- **Key = deutscher Klartext** (kein Dot-Notation-Schema) +- Kein Plural-Framework -- separate Keys verwenden: `t('1 Eintrag')` vs. `t('{count} Eintraege', { count })` +- Fehlende Keys erscheinen als `[Text]` (eckige Klammern = unuebersetzt) +- Admin synchronisiert Sprachsets ueber Administration → System → UI-Sprachen → "Alle aktualisieren" + ## Backend (FastAPI/Python) - **Pydantic-Models** als einzige Quelle fuer UI-Feld-Definitionen diff --git a/d-guides/doc-sync.mdc b/d-guides/doc-sync.mdc index 3ff3bb2..dada1bf 100644 --- a/d-guides/doc-sync.mdc +++ b/d-guides/doc-sync.mdc @@ -14,6 +14,7 @@ When a code change touches any of these areas, remind the user to update the wik - Navigation / routing structure - Feature boundaries (new feature module, moved responsibilities) - Billing / subscription logic +- UI text changes (all visible UI strings must use `t()` for i18n -- see `wiki/d-guides/coding-conventions.md`) ## During active work