cleaned sql and ui language sets

This commit is contained in:
ValueOn AG 2026-04-08 20:29:18 +02:00
parent 7f3170f324
commit fe9e5b6b89
9 changed files with 1052 additions and 408 deletions

View file

@ -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

View file

@ -1,5 +1,5 @@
<!-- status: canonical -->
<!-- lastReviewed: 2026-04-07 -->
<!-- lastReviewed: 2026-04-08 -->
<!-- verifiedAgainst: frontend_nyla (codebase audit 2026-04-07, post Automation Unification) -->
# 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 `<Outlet />` (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

View file

@ -1,5 +1,5 @@
<!-- status: canonical -->
<!-- lastReviewed: 2026-04-07 -->
<!-- lastReviewed: 2026-04-08 -->
<!-- verifiedAgainst: gateway (codebase audit 2026-04-07, post Automation Unification) -->
# 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 |

View file

@ -1,398 +0,0 @@
<!-- status: draft -->
<!-- lastReviewed: 2026-04-07 -->
# 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<string, unknown> {
// 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 (1015 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 | 23 Tage | Keine |
| 2 — Output-Normalizer | 35 Tage | Phase 1 |
| 3 — Frontend DataPicker aus Schema | 23 Tage | Phase 1 |
| 4 — Generisches Parameter-Rendering | 34 Tage | Phase 1 |
| 5 — Data Nodes (aggregate, transform, filter) | 35 Tage | Phase 2 |
| **Gesamt** | **~1320 Tage** | Phasen 13 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` |

View file

@ -0,0 +1,123 @@
<!-- status: plan -->
<!-- started: 2026-04-08 -->
<!-- component: gateway -->
# 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: <uuid>`; `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/`

View file

@ -0,0 +1,694 @@
<!-- status: draft -->
<!-- lastReviewed: 2026-04-07 -->
# 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 |

View file

@ -0,0 +1,195 @@
<!-- status: done -->
<!-- started: 2026-04-08 -->
<!-- completed: 2026-04-08 -->
<!-- component: gateway | frontend-nyla -->
# 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`

View file

@ -1,4 +1,4 @@
<!-- status: canonical -->
doku nachgeführt werden an <!-- status: canonical -->
<!-- lastReviewed: 2026-04-05 -->
# 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

View file

@ -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