cleaned sql and ui language sets
This commit is contained in:
parent
7f3170f324
commit
fe9e5b6b89
9 changed files with 1052 additions and 408 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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 (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` |
|
||||
123
c-work/1-plan/2026-04-gateway-int-stability-and-bugfixes.md
Normal file
123
c-work/1-plan/2026-04-gateway-int-stability-and-bugfixes.md
Normal 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/`
|
||||
694
c-work/2-build/2026-04-generic-graph-editor.md
Normal file
694
c-work/2-build/2026-04-generic-graph-editor.md
Normal 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 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 <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 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 |
|
||||
195
c-work/4-done/2026-04-ui-i18n-dynamic-language-sets.md
Normal file
195
c-work/4-done/2026-04-ui-i18n-dynamic-language-sets.md
Normal 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`
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue