updated concept documents
This commit is contained in:
parent
c8878489b5
commit
92c9e37f1e
10 changed files with 2648 additions and 24 deletions
|
|
@ -705,14 +705,14 @@ async def callAiPlanning(
|
|||
**Characteristics**:
|
||||
- Returns unified `AiResponse` model
|
||||
- Uses `parseJsonWithModel()` for structured parsing
|
||||
- modelName, priceUsd, processingTime tracked separately (not in response)
|
||||
- modelName, priceCHF, processingTime tracked separately (not in response)
|
||||
|
||||
**Calls**:
|
||||
1. `self._buildPromptWithPlaceholders(prompt, placeholdersDict)` → `str`
|
||||
2. `AiCallRequest(prompt=fullPrompt, context="", options=options)` - Create request
|
||||
3. `self.aiObjects.call(request)` → `AiCallResponse`
|
||||
4. `AiResponse(content=response.content, metadata=None)` → `AiResponse` ✅
|
||||
- Note: modelName, priceUsd, processingTime tracked separately (e.g., ChatStat)
|
||||
- Note: modelName, priceCHF, processingTime tracked separately (e.g., ChatStat)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -694,7 +694,7 @@ async def processContentPartsWithAi(
|
|||
return AiCallResponse(
|
||||
content=mergedContent,
|
||||
modelName="multiple",
|
||||
priceUsd=sum(r.priceUsd for r in allResults),
|
||||
priceCHF=sum(r.priceCHF for r in allResults),
|
||||
processingTime=sum(r.processingTime for r in allResults),
|
||||
bytesSent=sum(r.bytesSent for r in allResults),
|
||||
bytesReceived=sum(r.bytesReceived for r in allResults),
|
||||
|
|
|
|||
|
|
@ -901,7 +901,7 @@ AiCallRequest:
|
|||
AiCallResponse:
|
||||
- content: str
|
||||
- modelName: str
|
||||
- priceUsd: float
|
||||
- priceCHF: float
|
||||
- processingTime: float
|
||||
- bytesSent: int
|
||||
- bytesReceived: int
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ G: Idea
|
|||
│ │ │ ├── _callWithModel(model, prompt, context, temperature, maxTokens, inputBytes)
|
||||
│ │ │ ├── If fails → try next model in fallback list
|
||||
│ │ │ └── If all fail → return error
|
||||
│ │ └── Returns: AiCallResponse with content, modelName, priceUsd, etc.
|
||||
│ │ └── Returns: AiCallResponse with content, modelName, priceCHF, etc.
|
||||
|
||||
|
||||
|
||||
|
|
@ -106,7 +106,7 @@ The architecture for interfaceAiObjects.call + interfaceAiObjects.callImage chan
|
|||
5.4. chunking: if chunkItem size bigger than models maxSize: to produce chunks for the chunkItem, then replace the chunkItem in pipelineContentPartChunks[] with the new chunks
|
||||
5.5. processing: process chunkItem's in pipelineContentPartChunks and write each processed extraction into processedContentPartChunks and remove chunkItem in pipelineContentPartChunks. This to loop until model fails (to increment model fail counter for the model, to increment model index number, then next loop) or no items anymore (to exit LOOP).
|
||||
|
||||
6. Returns: AiCallResponse with content, modelName, priceUsd, etc. based on the delivered data in processedContentPartChunks
|
||||
6. Returns: AiCallResponse with content, modelName, priceCHF, etc. based on the delivered data in processedContentPartChunks
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
1064
concepts/Billing-Konzept.md
Normal file
1064
concepts/Billing-Konzept.md
Normal file
File diff suppressed because it is too large
Load diff
161
implementation/doc_automation_templates_db_and_editor_concept.md
Normal file
161
implementation/doc_automation_templates_db_and_editor_concept.md
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
# Automation Templates in DB und Editor – Konzept
|
||||
|
||||
## Übersicht
|
||||
|
||||
Templates werden von der Python-Datei in die Datenbank verschoben. Placeholders gehören nicht zum Template, sondern nur zur konkreten **Anwendung** (AutomationDefinition). Template-Liste und **CRUD auf Templates** (Anlegen/Ändern/Löschen) gemäss **RBAC** – Nutzer verwalten ihre **eigenen** Templates (z.B. MY-Level); SysAdmin macht nur den **Bootstrap** (initialer Seed). Im UI: **Verwaltung der Automation-Objekte** (bereits vorhanden) und ein **Automation Editor** mit Modus „Definition“ oder „Template“.
|
||||
|
||||
---
|
||||
|
||||
## 1. Datenmodell
|
||||
|
||||
### 1.1 Bestehend: AutomationDefinition (weiter nutzbar)
|
||||
|
||||
- **Datei:** `gateway/modules/features/automation/datamodelFeatureAutomation.py`
|
||||
- **Tabelle:** `AutomationDefinition` (Namespace `data.automation`), RBAC bereits: MY-Level (user-owned).
|
||||
- **Felder (Auszug):**
|
||||
- `id`, `mandateId`, `featureInstanceId`, `label`, `schedule`, `active`, `eventId`, `status`, `executionLogs`
|
||||
- **`template`** (string): JSON des Workflow-Plans. Wird aus einem **AutomationTemplate** übernommen, wenn der User „Aus Template erstellen“ wählt – danach keine Referenz mehr, die Definition kann unabhängig weiterbearbeitet werden. Kann auch frei (ohne Template) gepflegt werden.
|
||||
- **`placeholders`** (Dict[str, str]): Nur hier – Werte für `{{KEY:name}}` bei der **spezifischen Anwendung**. Templates enthalten keine Placeholders.
|
||||
|
||||
**Fazit:** AutomationDefinition bleibt zentrales Modell; **keine** `templateId`-Referenz. Beim Erzeugen aus einem Template wird der Template-Inhalt in die Definition kopiert; danach wird nur noch die Definition im Editor bearbeitet. Eine Definition kann ihrerseits **als neues Template gespeichert** werden – Berechtigung dazu gemäss **RBAC** (wer create auf AutomationTemplate hat).
|
||||
|
||||
### 1.2 Neu: AutomationTemplate (nur DB, ohne Placeholders)
|
||||
|
||||
- **Zweck:** Vordefinierte Workflow-Vorlagen **ohne** Platzhalter. CRUD gemäss **RBAC** – Nutzer ihre eigenen (z.B. MY); Bootstrap (initialer Seed) nur durch SysAdmin/System.
|
||||
- **Vorschlag Felder:**
|
||||
- `id` (PK, UUID)
|
||||
- `label` (**MultiLanguage**, z.B. `{"en": "SharePoint Summary", "de": "SharePoint Zusammenfassung"}`): Anzeigename z.B. „SharePoint Themen Zusammenfassung“
|
||||
- `overview` (**MultiLanguage**, optional): Kurzbeschreibung
|
||||
- **`template`** (string): JSON der Workflow-Struktur **mit** Platzhaltern im Format `{{KEY:placeholderName}}`. Enthält z.B. `overview`, `tasks` mit `actionList`, `execMethod`, `execAction`, `execParameters` (dort die Keys). Keine konkreten Werte für Placeholders – die kommen erst in AutomationDefinition.placeholders.
|
||||
- `_createdAt`, `_updatedAt`, `_createdBy` (Systemfelder, optional je nach bestehendem Muster)
|
||||
|
||||
**Keine** `placeholders`-Spalte in AutomationTemplate. Placeholders existieren nur in AutomationDefinition bei der Nutzung eines Templates.
|
||||
|
||||
### 1.3 Beziehung (ohne Referenz)
|
||||
|
||||
- Beim **„Aus Template erstellen“**: Inhalt von `AutomationTemplate.template` wird in eine **neue** AutomationDefinition kopiert (`template` + leere oder vorgeschlagene `placeholders`). Das **Label** wird in der **Sprache des aktuellen Users** übernommen (`definition.label = template.label[userLanguage]`). Es wird **keine** Referenz (templateId) angelegt – die Definition ist danach eigenständig und kann beliebig geändert werden.
|
||||
- Eine **bestehende AutomationDefinition** wird ausschließlich im Editor bearbeitet (template + placeholders).
|
||||
- **„Als Template speichern“**: Aus einer AutomationDefinition kann ein neues AutomationTemplate angelegt werden – Inhalt von `template` wird übernommen, **ohne** konkrete Placeholder-Werte (nur `{{KEY:...}}` bleibt). Berechtigung gemäss **RBAC** (wer create auf AutomationTemplate hat).
|
||||
|
||||
---
|
||||
|
||||
## 2. RBAC und Bootstrap
|
||||
|
||||
### 2.1 AutomationTemplate
|
||||
|
||||
- **Bootstrap:** Beim ersten Start (oder Migration) werden die bisherigen Vorlagen aus `subAutomationTemplates.py` in die Tabelle **AutomationTemplate** eingespielt – **ohne scharfe Parameter** (keine konkreten Placeholder-Werte aus den bisherigen `parameters`-Blöcken). Nur die reinen Template-JSON-Strukturen mit `{{KEY:...}}`.
|
||||
- **RBAC:** Gemäss RBAC-System – Sichtbarkeit und **CREATE/UPDATE/DELETE** auf AutomationTemplate nach RBAC-Regeln (z.B. Nutzer können **ihre eigenen** Templates anlegen/ändern/löschen, MY-Level). SysAdmin nur für **Bootstrap** (initialer Seed der Vorlagen aus Python).
|
||||
- **UI-Context:** `ui.feature.automation.templates` – Sichtbarkeit und Aktionen (lesen, erstellen, bearbeiten, löschen) nach RBAC.
|
||||
|
||||
### 2.2 AutomationDefinition (unverändert)
|
||||
|
||||
- SysAdmin: kann alle AutomationDefinition-Einträge sehen/bearbeiten (z.B. über bestehende RBAC-Logik mit mandateId/context).
|
||||
- User: nur eigene AutomationDefinition-Einträge (MY). Scheduling-Verwaltung: SysAdmin kann alle Schedulings bearbeiten, jeder User nur die eigenen Automations.
|
||||
|
||||
---
|
||||
|
||||
## 3. Backend-Logik
|
||||
|
||||
### 3.1 AutomationTemplate CRUD
|
||||
|
||||
- **Neue API** unter z.B. `/api/automation-templates` (oder unter bestehendem Router mit Präfix). **Zugriff gemäss RBAC** – Nutzer sehen/bearbeiten/löschen nur ihre eigenen Templates (z.B. MY; Filter/Prüfung über `_createdBy` bzw. RBAC-Regeln für `data.automation.AutomationTemplate`).
|
||||
- `GET /api/automation-templates` – Liste (evtl. paginiert), gefiltert nach RBAC
|
||||
- `GET /api/automation-templates/{id}` – Einzelabruf (nur wenn berechtigt)
|
||||
- `POST /api/automation-templates` – Anlegen (wer create hat)
|
||||
- `PUT /api/automation-templates/{id}` – Aktualisieren (wer update auf diesen Eintrag hat)
|
||||
- `DELETE /api/automation-templates/{id}` – Löschen (wer delete auf diesen Eintrag hat)
|
||||
- **Bootstrap/Migration (nur SysAdmin/System):** Einmalig Daten aus `subAutomationTemplates.py` lesen, pro „template“-Eintrag einen AutomationTemplate-Datensatz anlegen (nur `label`/`overview` + JSON-`template` mit `{{KEY:...}}`, keine `parameters` speichern). Danach CRUD nur noch über RBAC.
|
||||
|
||||
### 3.2 Template-API und Sichtbarkeit
|
||||
|
||||
- **GET /api/automations/templates** (bzw. neuer Endpoint für AutomationTemplate): Liste aus **DB** (AutomationTemplate). **Sichtbarkeit gemäss RBAC** – Nutzer sehen die Templates, auf die sie Zugriff haben (z.B. eigene + ggf. Gruppen); CREATE/UPDATE/DELETE über gleiche API, Berechtigung pro Request per RBAC.
|
||||
|
||||
### 3.3 Nutzbare Actions (Katalog, RBAC-gefiltert)
|
||||
|
||||
- **Endpoint** z.B. `GET /api/automations/actions`:
|
||||
- **Datenquelle:** Pro Action die bestehende **WorkflowActionDefinition** nutzen (`actionId`, `description`, `parameters` mit Typ, `frontendType`, etc.) – diese Daten sind pro Action bereits verfügbar (Method-Basis / methodDiscovery).
|
||||
- **RBAC:** Nur Actions zurückgeben, für die der aktuelle User Berechtigung hat (RESOURCE-Context, `actionId` – z.B. `_checkActionPermission(actionId)`). So entsteht die „Übersicht aller nutzbaren Actions“ nach RBAC.
|
||||
- **Format:** Pro Action die Action-Definition ausliefern (actionId, description, parameters-Schema); optional ein **Beispiel-JSON-Snippet** pro Action (minimales action-Objekt: execMethod, execAction, execParameters, execResultLabel) für Copy/Paste ins Template-JSON.
|
||||
|
||||
### 3.4 JSON-Validierung und Placeholder-Extraktion
|
||||
|
||||
- Beim Speichern einer AutomationDefinition im Editor (oder in einem separaten „Validate“-Schritt):
|
||||
- **JSON validieren:** `template`-String parsen, Syntax-Check; bei Fehler Meldung anzeigen.
|
||||
- **Placeholder-Extraktion:** Regex oder Parser über den `template`-String, alle Vorkommen von `{{KEY:placeholderName}}` sammeln und als vorgeschlagene Keys in `placeholders` anzeigen bzw. bestehende `placeholders` anpassen (neue Keys hinzufügen, nicht mehr vorkommende optional entfernen oder behalten).
|
||||
|
||||
---
|
||||
|
||||
## 4. UI – Admin-Seiten (frontend_nyla)
|
||||
|
||||
### 4.1 Seite: Verwaltung der Automation-Objekte und Scheduling (existiert)
|
||||
|
||||
- **Route/Seite:** Bereits vorhanden (z.B. AutomationsPage, Route `automations`).
|
||||
- **Funktion:** Liste der AutomationDefinition-Einträge; Erstellen, Bearbeiten, Löschen, Ausführen, Logs.
|
||||
- **Berechtigung:** SysAdmin kann alle Automations und deren Scheduling bearbeiten; normale User nur die eigenen. Entspricht dem bestehenden MY-Level plus Admin-Logik (z.B. isSysAdmin sieht alle).
|
||||
|
||||
### 4.2 Seite: Automation Editor (neu) – ein Editor, zwei Modi
|
||||
|
||||
- **Zweck:** Im Editor wird **entweder** ein **AutomationTemplate** bearbeitet **oder** eine **AutomationDefinition** erstellt/bearbeitet. Entscheidend ist ein **Modus-Flag** im Editor (nicht templateId).
|
||||
|
||||
**Modus-Flag im Editor:**
|
||||
|
||||
- **„Definition bearbeiten / neu erstellen“** (Standard für normale User):
|
||||
- Inhalt kommt aus einer bestehenden AutomationDefinition oder aus „Aus Template erstellen“ (dann wird Template-Inhalt in eine **neue** Definition kopiert).
|
||||
- Speichern → **AutomationDefinition** wird erstellt bzw. aktualisiert (template + placeholders). Keine Referenz zum Template.
|
||||
|
||||
- **„Template bearbeiten“**:
|
||||
- Inhalt kommt aus einem AutomationTemplate (oder „Neu“ für leeres Template). Sichtbar/bearbeitbar gemäss **RBAC** (Nutzer nur eigene).
|
||||
- Speichern → **AutomationTemplate** wird erstellt bzw. aktualisiert. Keine placeholders im Template.
|
||||
|
||||
- **„Als Template speichern“** (von einer Definition aus): Aus der aktuell geöffneten AutomationDefinition wird ein neues AutomationTemplate angelegt (nur template-JSON mit `{{KEY:...}}`, keine scharfen Werte). Erlaubt für Nutzer, die gemäss **RBAC** create auf AutomationTemplate haben.
|
||||
|
||||
**Saubere Trennung:** Derselbe Editor (gleiche UI: JSON + Platzhalter-Key/Value + Actions-Katalog). Nur das Zielobjekt und die Speicher-Logik unterscheiden sich je nach Modus: Save schreibt entweder in AutomationDefinition oder in AutomationTemplate. Keine templateId nötig.
|
||||
|
||||
**Editor-Funktionen (unabhängig vom Modus):**
|
||||
|
||||
1. **JSON bearbeiten (Text, Copy/Paste, Validierung)**
|
||||
- Großes Textfeld (oder Code-Editor) mit dem Inhalt von `template` (aus Definition oder Template).
|
||||
- Nach Bearbeitung: „Validieren“-Button oder beim Speichern: JSON parsen; bei Fehler Meldung anzeigen.
|
||||
- Nach Validierung: **Platzhalter anpassen** – aus dem JSON alle `{{KEY:xyz}}` extrahieren und in der Placeholder-Verwaltung als Keys anzeigen/aktualisieren. (Bei Template-Modus: Placeholder-UI nur zur Anzeige/Vorschau; gespeichert wird nur das Template ohne Werte.)
|
||||
|
||||
2. **Platzhalter als dynamische Key/Value-Objekte**
|
||||
- Liste aller in `template` vorkommenden Placeholder-Keys (aus Extraktion).
|
||||
- Pro Key: Wert eingeben. **Typ:** Aus der **Action-Definition** (WorkflowActionParameter, frontendType) der Action, in der der Key vorkommt.
|
||||
- Werte werden nur bei **Definition** gespeichert (`AutomationDefinition.placeholders`). Beim Template-Modus keine Werte persistiert.
|
||||
|
||||
3. **Übersicht nutzbarer Actions (RBAC)**
|
||||
- Bereich „Nutzbare Actions“: Daten von `GET /api/automations/actions` – pro Action die **Action-Definition** (actionId, description, parameters-Schema).
|
||||
- **Copy/Paste:** Pro Action ein JSON-Snippet (action-Objekt) anzeigen, Buttons „Copy JSON“ / „Insert at cursor“ zum Einfügen ins Template-JSON.
|
||||
|
||||
**Navigation:** Von der Automations-Verwaltung: „Neu“ (leer oder „Aus Template erstellen“) bzw. „Bearbeiten“ → Editor im **Definitions-Modus**. Wer gemäss RBAC Templates verwalten darf: „Template bearbeiten“ / „Templates verwalten“ → Editor im **Template-Modus** oder Liste AutomationTemplate (nur eigene bzw. nach RBAC sichtbare).
|
||||
|
||||
### 4.3 Template-Verwaltung
|
||||
|
||||
- **Seite:** „Templates verwalten“. Sichtbar/verfügbar gemäss **RBAC** – Nutzer sehen und verwalten **ihre eigenen** Templates (CRUD nach RBAC, z.B. MY).
|
||||
- **Inhalt:** CRUD für **AutomationTemplate**. Liste mit Bearbeiten/Neu/Löschen; beim Bearbeiten: `label`, `overview`, `template` (JSON mit `{{KEY:...}}`, ohne konkrete Placeholder-Werte). Template-Liste gefiltert nach Berechtigung („Aus Template erstellen“ nutzt dieselbe Liste).
|
||||
|
||||
---
|
||||
|
||||
## 5. Zusammenfassung
|
||||
|
||||
| Thema | Inhalt |
|
||||
|------|--------|
|
||||
| **Datenmodell** | AutomationDefinition unverändert, **keine** templateId. Neu: AutomationTemplate (nur DB, ohne placeholders). Definition aus Template = Kopie des Inhalts; Definition kann als Template gespeichert werden. |
|
||||
| **Templates** | Von Python in DB; Bootstrap: nur Template-JSON mit `{{KEY:...}}`, **keine scharfen Parameter**. |
|
||||
| **Placeholders** | Nur in AutomationDefinition; nie im Template. |
|
||||
| **RBAC Templates** | Sichtbarkeit und CREATE/UPDATE/DELETE gemäss RBAC – Nutzer ihre **eigenen** Templates (z.B. MY); SysAdmin nur **Bootstrap** (initialer Seed). |
|
||||
| **RBAC Automations** | Unverändert: SysAdmin alle, User nur eigene (Scheduling + Definition). |
|
||||
| **Backend** | AutomationTemplate CRUD gemäss **RBAC** (Nutzer eigene); Bootstrap nur SysAdmin/System; GET nutzbare Actions mit **Daten pro Action-Definition** (WorkflowActionDefinition), RBAC-gefiltert; JSON-Validierung + Placeholder-Extraktion. |
|
||||
| **UI** | Verwaltung Automations + Scheduling (existiert); ein **Automation Editor** mit **Modus-Flag**: Template bearbeiten vs. Definition erstellen/bearbeiten; „Als Template speichern“ aus Definition (gemäss RBAC). |
|
||||
|
||||
---
|
||||
|
||||
## 6. Erledigte Klarstellungen
|
||||
|
||||
- **Keine templateId:** Eine AutomationDefinition wird aus einem Template **erzeugt** (Inhalt kopiert), danach keine Referenz – die Definition wird im Editor bearbeitet und kann sich vom Template unterscheiden. Eine Definition kann ihrerseits **als neues Template gespeichert** werden – Berechtigung gemäss **RBAC**.
|
||||
- **Bootstrap:** Templates dürfen **keine scharfen Parameter** haben (nur `{{KEY:...}}`). **Nur SysAdmin/System** führt den initialen Bootstrap (Seed aus Python) aus; danach CRUD auf Templates **gemäss RBAC** (Nutzer ihre eigenen).
|
||||
- **GET nutzbare Actions:** **Daten pro Action-Definition** nutzen (WorkflowActionDefinition) – pro Action verfügbar.
|
||||
- **Template-Liste und CRUD:** Sichtbarkeit und CREATE/UPDATE/DELETE AutomationTemplate **gemäss RBAC** – Nutzer verwalten ihre **eigenen** Templates; SysAdmin nur Bootstrap.
|
||||
- **Editor sauber mit Modus-Flag:** Ein Flag im Editor steuert, ob ein **Template** oder eine **Definition** bearbeitet wird. Je nachdem schreibt Speichern in AutomationTemplate oder AutomationDefinition. „Als Template speichern“ erzeugt aus einer Definition ein neues Template – erlaubt für jeden mit RBAC-Recht create auf AutomationTemplate.
|
||||
|
||||
Dieses Dokument kann als Basis für Implementierungs-Tickets (Backend: Modelle, API, Bootstrap; Frontend: Editor mit Modus, Template-Verwaltung) verwendet werden.
|
||||
933
implementation/doc_automation_templates_impl_frontend.md
Normal file
933
implementation/doc_automation_templates_impl_frontend.md
Normal file
|
|
@ -0,0 +1,933 @@
|
|||
# Automation Templates – Frontend Implementation Concept
|
||||
|
||||
**Basis:** `doc_automation_templates_db_and_editor_concept.md`
|
||||
|
||||
---
|
||||
|
||||
## ✅ STATUS: IMPLEMENTIERT (2026-02-03)
|
||||
|
||||
**Backend ist vollständig implementiert:**
|
||||
- ✅ API: `GET/POST/PUT/DELETE /api/automation-templates`
|
||||
- ✅ Model: `AutomationTemplate` mit `TextMultilingual` Feldern
|
||||
- ✅ Navigation: `/workflows/automation-templates` in mainSystem.py
|
||||
- ✅ API: `GET /api/automations/actions` - Actions-Katalog
|
||||
|
||||
**Frontend implementiert:**
|
||||
| Komponente | Status |
|
||||
|------------|--------|
|
||||
| `automationApi.ts` - Typen + API-Funktionen | ✅ Implementiert |
|
||||
| `useAutomations.ts` - Hooks (useWorkflowActions) | ✅ Implementiert |
|
||||
| `AutomationTemplatesPage.tsx` | ✅ Implementiert |
|
||||
| `AutomationEditor.tsx` | ✅ **NEU** - Full-screen Editor |
|
||||
| `ActionsPanel.tsx` | ✅ Implementiert |
|
||||
| `App.tsx` - Route | ✅ Implementiert |
|
||||
| `pageRegistry.tsx` - Icon | ✅ Implementiert |
|
||||
|
||||
**Neue Dateien:**
|
||||
- `src/components/AutomationEditor/AutomationEditor.tsx`
|
||||
- `src/components/AutomationEditor/AutomationEditor.module.css`
|
||||
- `src/components/AutomationEditor/index.ts`
|
||||
|
||||
---
|
||||
|
||||
## WICHTIGE HINWEISE
|
||||
|
||||
### FormGenerator: Automatische Multilingual-Unterstützung
|
||||
|
||||
| Komponente | TextMultilingual-Handling |
|
||||
|------------|---------------------------|
|
||||
| **FormGeneratorTable** | ✅ **Automatisch** - erkennt TextMultilingual und rendert in User-Sprache |
|
||||
| **FormGeneratorForm** | ✅ **Automatisch** - mit `type: 'multilingual'` in Attribut-Definition |
|
||||
|
||||
**Kein manueller Code nötig!** Einfach Daten mit TextMultilingual-Feldern übergeben.
|
||||
|
||||
### Bestehende vs. Neue API-Typen
|
||||
|
||||
| Typ | Quelle | Verwendung |
|
||||
|-----|--------|------------|
|
||||
| `AutomationTemplate` | `subAutomationTemplates.py` (Legacy) | Alte hardcoded Templates |
|
||||
| `AutomationTemplateDB` | DB (neu) | Neue datenbankbasierte Templates |
|
||||
|
||||
Die bestehende `fetchAutomationTemplates()` Funktion lädt Legacy-Templates. Für die neuen DB-Templates wird `fetchAutomationTemplatesDB()` verwendet.
|
||||
|
||||
---
|
||||
|
||||
## 1. Übersicht der Komponenten
|
||||
|
||||
| Komponente | Zweck | Status |
|
||||
|------------|-------|--------|
|
||||
| `automationApi.ts` | API-Funktionen für Templates + Actions | Erweitern |
|
||||
| `useAutomations.ts` | Hooks für Templates + Actions | Erweitern |
|
||||
| `AutomationsPage.tsx` | Liste der Definitions; öffnet Editor | Erweitern |
|
||||
| `AutomationTemplatesPage.tsx` | Liste der Templates (CRUD) | **Neu** |
|
||||
| `AutomationEditor.tsx` | Editor mit Modus-Flag (Definition/Template) | **Neu** |
|
||||
| `ActionsPanel.tsx` | Actions-Katalog mit Copy/Paste | **Neu** |
|
||||
|
||||
---
|
||||
|
||||
## 2. API-Erweiterungen (automationApi.ts)
|
||||
|
||||
### 2.1 Neuer Typ: AutomationTemplate (DB-Version, MultiLanguage)
|
||||
|
||||
```typescript
|
||||
// Multilingual text type (matches backend TextMultilingual)
|
||||
export interface TextMultilingual {
|
||||
en: string;
|
||||
ge?: string;
|
||||
fr?: string;
|
||||
it?: string;
|
||||
}
|
||||
|
||||
// NEW: AutomationTemplate from DB (not the old in-memory structure)
|
||||
export interface AutomationTemplateDB {
|
||||
id: string;
|
||||
label: TextMultilingual;
|
||||
overview?: TextMultilingual;
|
||||
template: string; // JSON string with {{KEY:...}} placeholders
|
||||
_createdAt?: number;
|
||||
_createdBy?: string;
|
||||
_createdByUserName?: string;
|
||||
}
|
||||
|
||||
// Action definition from backend
|
||||
export interface WorkflowAction {
|
||||
method: string;
|
||||
action: string;
|
||||
actionId: string;
|
||||
description: string;
|
||||
category?: string;
|
||||
parameters: WorkflowActionParameter[];
|
||||
exampleJson: {
|
||||
execMethod: string;
|
||||
execAction: string;
|
||||
execParameters: Record<string, any>;
|
||||
execResultLabel: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface WorkflowActionParameter {
|
||||
name: string;
|
||||
type: string;
|
||||
frontendType: string;
|
||||
required: boolean;
|
||||
default?: any;
|
||||
description: string;
|
||||
frontendOptions?: string | string[];
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Neue API-Funktionen
|
||||
|
||||
```typescript
|
||||
// ============================================================================
|
||||
// AUTOMATION TEMPLATES (DB) API
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Fetch all automation templates (RBAC-filtered: own templates)
|
||||
* Endpoint: GET /api/automation-templates
|
||||
*/
|
||||
export async function fetchAutomationTemplatesDB(
|
||||
request: ApiRequestFunction
|
||||
): Promise<AutomationTemplateDB[]> {
|
||||
const data = await request({
|
||||
url: '/api/automation-templates',
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
if (data?.items && Array.isArray(data.items)) {
|
||||
return data.items;
|
||||
}
|
||||
return Array.isArray(data) ? data : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch single template by ID
|
||||
* Endpoint: GET /api/automation-templates/{id}
|
||||
*/
|
||||
export async function fetchAutomationTemplateById(
|
||||
request: ApiRequestFunction,
|
||||
templateId: string
|
||||
): Promise<AutomationTemplateDB | null> {
|
||||
try {
|
||||
return await request({
|
||||
url: `/api/automation-templates/${templateId}`,
|
||||
method: 'get'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching template:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new automation template
|
||||
* Endpoint: POST /api/automation-templates
|
||||
*/
|
||||
export async function createAutomationTemplateApi(
|
||||
request: ApiRequestFunction,
|
||||
templateData: Omit<AutomationTemplateDB, 'id' | '_createdAt' | '_createdBy'>
|
||||
): Promise<AutomationTemplateDB> {
|
||||
return await request({
|
||||
url: '/api/automation-templates',
|
||||
method: 'post',
|
||||
data: templateData
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update automation template
|
||||
* Endpoint: PUT /api/automation-templates/{id}
|
||||
*/
|
||||
export async function updateAutomationTemplateApi(
|
||||
request: ApiRequestFunction,
|
||||
templateId: string,
|
||||
templateData: Partial<AutomationTemplateDB>
|
||||
): Promise<AutomationTemplateDB> {
|
||||
return await request({
|
||||
url: `/api/automation-templates/${templateId}`,
|
||||
method: 'put',
|
||||
data: templateData
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete automation template
|
||||
* Endpoint: DELETE /api/automation-templates/{id}
|
||||
*/
|
||||
export async function deleteAutomationTemplateApi(
|
||||
request: ApiRequestFunction,
|
||||
templateId: string
|
||||
): Promise<void> {
|
||||
await request({
|
||||
url: `/api/automation-templates/${templateId}`,
|
||||
method: 'delete'
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// WORKFLOW ACTIONS API
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Fetch available workflow actions (RBAC-filtered)
|
||||
* Endpoint: GET /api/automations/actions
|
||||
*/
|
||||
export async function fetchWorkflowActions(
|
||||
request: ApiRequestFunction
|
||||
): Promise<WorkflowAction[]> {
|
||||
const data = await request({
|
||||
url: '/api/automations/actions',
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return data?.actions || [];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Hooks-Erweiterungen (useAutomations.ts)
|
||||
|
||||
### 3.1 Neuer Hook: useAutomationTemplates
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Hook for managing AutomationTemplates (DB)
|
||||
*/
|
||||
export function useAutomationTemplates() {
|
||||
const [templates, setTemplates] = useState<AutomationTemplateDB[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { request } = useApiRequest();
|
||||
const { checkPermission } = usePermissions();
|
||||
const [permissions, setPermissions] = useState<UserPermissions | null>(null);
|
||||
|
||||
const fetchTemplates = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await fetchAutomationTemplatesDB(request);
|
||||
setTemplates(data);
|
||||
} catch (e: any) {
|
||||
setError(e.message);
|
||||
setTemplates([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [request]);
|
||||
|
||||
const fetchPermissions = useCallback(async () => {
|
||||
const perms = await checkPermission('DATA', 'AutomationTemplate');
|
||||
setPermissions(perms);
|
||||
return perms;
|
||||
}, [checkPermission]);
|
||||
|
||||
const createTemplate = useCallback(async (data: Omit<AutomationTemplateDB, 'id'>) => {
|
||||
return await createAutomationTemplateApi(request, data);
|
||||
}, [request]);
|
||||
|
||||
const updateTemplate = useCallback(async (id: string, data: Partial<AutomationTemplateDB>) => {
|
||||
return await updateAutomationTemplateApi(request, id, data);
|
||||
}, [request]);
|
||||
|
||||
const deleteTemplate = useCallback(async (id: string) => {
|
||||
await deleteAutomationTemplateApi(request, id);
|
||||
}, [request]);
|
||||
|
||||
return {
|
||||
templates,
|
||||
loading,
|
||||
error,
|
||||
permissions,
|
||||
fetchTemplates,
|
||||
fetchPermissions,
|
||||
createTemplate,
|
||||
updateTemplate,
|
||||
deleteTemplate,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Neuer Hook: useWorkflowActions
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Hook for fetching available workflow actions (RBAC-filtered)
|
||||
*/
|
||||
export function useWorkflowActions() {
|
||||
const [actions, setActions] = useState<WorkflowAction[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { request } = useApiRequest();
|
||||
|
||||
const fetchActions = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await fetchWorkflowActions(request);
|
||||
setActions(data);
|
||||
} catch (e) {
|
||||
console.error('Error fetching actions:', e);
|
||||
setActions([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [request]);
|
||||
|
||||
return { actions, loading, fetchActions };
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Neue Seite: AutomationTemplatesPage.tsx
|
||||
|
||||
### 4.1 Imports
|
||||
|
||||
```typescript
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { FaSync, FaPlus, FaFileAlt } from 'react-icons/fa';
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||
import { useAutomationTemplates, AutomationTemplateDB, TextMultilingual } from '../../hooks/useAutomations';
|
||||
import { AutomationEditor } from '../../components/AutomationEditor'; // Neu
|
||||
import styles from '../admin/Admin.module.css';
|
||||
```
|
||||
|
||||
### 4.2 Struktur
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* AutomationTemplatesPage
|
||||
*
|
||||
* CRUD für AutomationTemplates (eigene Templates verwalten).
|
||||
* Nutzt FormGeneratorTable wie AutomationsPage.
|
||||
*/
|
||||
export const AutomationTemplatesPage: React.FC = () => {
|
||||
const {
|
||||
templates,
|
||||
loading,
|
||||
permissions,
|
||||
fetchTemplates,
|
||||
fetchPermissions,
|
||||
createTemplate,
|
||||
updateTemplate,
|
||||
deleteTemplate,
|
||||
} = useAutomationTemplates();
|
||||
|
||||
const [showEditor, setShowEditor] = useState(false);
|
||||
const [editingTemplate, setEditingTemplate] = useState<AutomationTemplateDB | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTemplates();
|
||||
fetchPermissions();
|
||||
}, []);
|
||||
|
||||
// Check permissions
|
||||
const canCreate = permissions?.create !== 'n';
|
||||
const canUpdate = permissions?.update !== 'n';
|
||||
const canDelete = permissions?.delete !== 'n';
|
||||
|
||||
// Open Editor in "Template mode"
|
||||
const handleEdit = (template: AutomationTemplateDB) => {
|
||||
setEditingTemplate(template);
|
||||
setShowEditor(true);
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
setEditingTemplate(null); // New template
|
||||
setShowEditor(true);
|
||||
};
|
||||
|
||||
const handleSave = async (data: Partial<AutomationTemplateDB>) => {
|
||||
if (editingTemplate) {
|
||||
await updateTemplate(editingTemplate.id, data);
|
||||
} else {
|
||||
await createTemplate(data as any);
|
||||
}
|
||||
setShowEditor(false);
|
||||
fetchTemplates();
|
||||
};
|
||||
|
||||
// Columns - FormGeneratorTable rendert TextMultilingual automatisch in User-Sprache!
|
||||
const columns = useMemo(() => [
|
||||
{ key: 'label', label: 'Label', type: 'string' as const, sortable: true, searchable: true },
|
||||
{ key: 'overview', label: 'Beschreibung', type: 'string' as const, width: 300 },
|
||||
{ key: '_createdByUserName', label: 'Erstellt von', type: 'string' as const },
|
||||
], []);
|
||||
|
||||
// Handle delete
|
||||
const handleDelete = async (template: AutomationTemplateDB) => {
|
||||
await deleteTemplate(template.id);
|
||||
fetchTemplates();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.adminPage}>
|
||||
<div className={styles.pageHeader}>
|
||||
<div>
|
||||
<h1 className={styles.pageTitle}>Automation-Vorlagen</h1>
|
||||
<p className={styles.pageSubtitle}>Verwalten Sie Ihre Workflow-Vorlagen</p>
|
||||
</div>
|
||||
<div className={styles.headerActions}>
|
||||
<button className={styles.secondaryButton} onClick={() => fetchTemplates()}>
|
||||
<FaSync /> Aktualisieren
|
||||
</button>
|
||||
{canCreate && (
|
||||
<button className={styles.primaryButton} onClick={handleCreate}>
|
||||
<FaPlus /> Neue Vorlage
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.tableContainer}>
|
||||
<FormGeneratorTable
|
||||
data={templates as any[]}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
pagination={true}
|
||||
pageSize={25}
|
||||
searchable={true}
|
||||
sortable={true}
|
||||
actionButtons={[
|
||||
...(canUpdate ? [{
|
||||
type: 'edit' as const,
|
||||
onAction: handleEdit,
|
||||
title: 'Bearbeiten',
|
||||
}] : []),
|
||||
...(canDelete ? [{
|
||||
type: 'delete' as const,
|
||||
title: 'Löschen',
|
||||
}] : []),
|
||||
]}
|
||||
onDelete={handleDelete}
|
||||
emptyMessage="Keine Vorlagen vorhanden"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showEditor && (
|
||||
<AutomationEditor
|
||||
mode="template"
|
||||
initialData={editingTemplate}
|
||||
onSave={handleSave}
|
||||
onCancel={() => setShowEditor(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Neue Komponente: AutomationEditor.tsx
|
||||
|
||||
### 5.1 Konzept: Saubere Lösung via FormGeneratorForm
|
||||
|
||||
**Kernidee:** FormGeneratorForm nutzen für ALLE Felder inkl. Multilingual!
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Attribute-Definitionen je nach Modus
|
||||
* FormGeneratorForm rendert type:'multilingual' automatisch mit Sprach-Tabs
|
||||
*/
|
||||
function getEditorAttributes(mode: 'definition' | 'template'): AttributeDefinition[] {
|
||||
const baseAttributes: AttributeDefinition[] = [
|
||||
{
|
||||
name: 'template',
|
||||
label: 'Template JSON',
|
||||
type: 'textarea',
|
||||
required: true,
|
||||
minRows: 15,
|
||||
maxRows: 30,
|
||||
description: 'Workflow-Definition als JSON'
|
||||
}
|
||||
];
|
||||
|
||||
if (mode === 'template') {
|
||||
return [
|
||||
{ name: 'label', label: 'Label', type: 'multilingual', required: true },
|
||||
{ name: 'overview', label: 'Beschreibung', type: 'multilingual' },
|
||||
...baseAttributes
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
{ name: 'label', label: 'Label', type: 'text', required: true },
|
||||
{ name: 'schedule', label: 'Schedule (Cron)', type: 'text' },
|
||||
{ name: 'active', label: 'Aktiv', type: 'checkbox' },
|
||||
...baseAttributes,
|
||||
{ name: 'placeholders', label: 'Platzhalter', type: 'textarea', description: 'JSON-Objekt' }
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Komponente
|
||||
|
||||
```typescript
|
||||
interface AutomationEditorProps {
|
||||
mode: 'definition' | 'template';
|
||||
initialData?: AutomationTemplateDB | Automation | null;
|
||||
onSave: (data: any) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const AutomationEditor: React.FC<AutomationEditorProps> = ({
|
||||
mode,
|
||||
initialData,
|
||||
onSave,
|
||||
onCancel,
|
||||
}) => {
|
||||
const [showActionsPanel, setShowActionsPanel] = useState(false);
|
||||
|
||||
// Daten für FormGeneratorForm vorbereiten
|
||||
const formData = useMemo(() => {
|
||||
if (!initialData) return {};
|
||||
return {
|
||||
...initialData,
|
||||
template: typeof initialData.template === 'string'
|
||||
? initialData.template
|
||||
: JSON.stringify(initialData.template, null, 2)
|
||||
};
|
||||
}, [initialData]);
|
||||
|
||||
return (
|
||||
<div className={styles.editorOverlay}>
|
||||
<div className={styles.editorContainer}>
|
||||
<header>
|
||||
<h2>{mode === 'template' ? 'Template bearbeiten' : 'Automation bearbeiten'}</h2>
|
||||
<button onClick={onCancel}><FaTimes /></button>
|
||||
</header>
|
||||
|
||||
<div className={styles.editorContent}>
|
||||
{/* Main Form - FormGeneratorForm macht alles! */}
|
||||
<div className={styles.editorMain}>
|
||||
<FormGeneratorForm
|
||||
attributes={getEditorAttributes(mode)}
|
||||
data={formData}
|
||||
mode={initialData ? 'edit' : 'create'}
|
||||
onSubmit={onSave}
|
||||
onCancel={onCancel}
|
||||
submitButtonText="Speichern"
|
||||
cancelButtonText="Abbrechen"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Actions Panel (optional, für Copy/Paste) */}
|
||||
<div className={styles.actionsPanel}>
|
||||
<button onClick={() => setShowActionsPanel(!showActionsPanel)}>
|
||||
{showActionsPanel ? 'Actions ausblenden' : 'Actions anzeigen'}
|
||||
</button>
|
||||
{showActionsPanel && <ActionsPanel />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**Vorteile dieser Lösung:**
|
||||
- Kein manueller Code für Multilingual-Inputs
|
||||
- FormGeneratorForm validiert automatisch
|
||||
- Konsistent mit dem Rest der Anwendung
|
||||
- Weniger Code, weniger Bugs
|
||||
|
||||
---
|
||||
|
||||
## 6. Neue Komponente: ActionsPanel.tsx
|
||||
|
||||
### 6.1 Struktur
|
||||
|
||||
```typescript
|
||||
interface ActionsPanelProps {
|
||||
onInsert: (actionJson: string) => void;
|
||||
onCopy?: (actionJson: string) => void;
|
||||
}
|
||||
|
||||
export const ActionsPanel: React.FC<ActionsPanelProps> = ({ onInsert, onCopy }) => {
|
||||
const { actions, loading, fetchActions } = useWorkflowActions();
|
||||
const [filter, setFilter] = useState('');
|
||||
const [expandedAction, setExpandedAction] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchActions();
|
||||
}, []);
|
||||
|
||||
const filteredActions = useMemo(() => {
|
||||
if (!filter) return actions;
|
||||
const lower = filter.toLowerCase();
|
||||
return actions.filter(a =>
|
||||
a.method.toLowerCase().includes(lower) ||
|
||||
a.action.toLowerCase().includes(lower) ||
|
||||
a.description.toLowerCase().includes(lower)
|
||||
);
|
||||
}, [actions, filter]);
|
||||
|
||||
// Group by method
|
||||
const groupedActions = useMemo(() => {
|
||||
const groups: Record<string, WorkflowAction[]> = {};
|
||||
filteredActions.forEach(action => {
|
||||
if (!groups[action.method]) {
|
||||
groups[action.method] = [];
|
||||
}
|
||||
groups[action.method].push(action);
|
||||
});
|
||||
return groups;
|
||||
}, [filteredActions]);
|
||||
|
||||
const handleCopy = (action: WorkflowAction) => {
|
||||
const json = JSON.stringify(action.exampleJson, null, 2);
|
||||
navigator.clipboard.writeText(json);
|
||||
onCopy?.(json);
|
||||
};
|
||||
|
||||
const handleInsert = (action: WorkflowAction) => {
|
||||
const json = JSON.stringify(action.exampleJson, null, 2);
|
||||
onInsert(json);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.actionsPanel}>
|
||||
<h3>Verfügbare Actions</h3>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Suchen..."
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
/>
|
||||
|
||||
{loading ? (
|
||||
<div>Lade Actions...</div>
|
||||
) : (
|
||||
<div className={styles.actionsList}>
|
||||
{Object.entries(groupedActions).map(([method, methodActions]) => (
|
||||
<div key={method} className={styles.methodGroup}>
|
||||
<h4>{method}</h4>
|
||||
{methodActions.map(action => (
|
||||
<div key={action.actionId} className={styles.actionItem}>
|
||||
<div
|
||||
className={styles.actionHeader}
|
||||
onClick={() => setExpandedAction(
|
||||
expandedAction === action.actionId ? null : action.actionId
|
||||
)}
|
||||
>
|
||||
<span className={styles.actionName}>
|
||||
{action.action}
|
||||
</span>
|
||||
<span className={styles.actionDesc}>
|
||||
{action.description}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{expandedAction === action.actionId && (
|
||||
<div className={styles.actionDetails}>
|
||||
<h5>Parameter:</h5>
|
||||
<ul>
|
||||
{action.parameters.map(p => (
|
||||
<li key={p.name}>
|
||||
<strong>{p.name}</strong>
|
||||
{p.required && <span className={styles.required}>*</span>}
|
||||
: {p.description}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<h5>Beispiel JSON:</h5>
|
||||
<pre>{JSON.stringify(action.exampleJson, null, 2)}</pre>
|
||||
|
||||
<div className={styles.actionButtons}>
|
||||
<button onClick={() => handleCopy(action)}>
|
||||
<FaCopy /> Kopieren
|
||||
</button>
|
||||
<button onClick={() => handleInsert(action)}>
|
||||
<FaPlus /> Einfügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Anpassungen in AutomationsPage.tsx
|
||||
|
||||
### 7.1 Editor-Integration
|
||||
|
||||
Bestehende `handleEditClick` und Template-Auswahl anpassen:
|
||||
|
||||
```typescript
|
||||
// State for editor
|
||||
const [showEditor, setShowEditor] = useState(false);
|
||||
const [editorMode, setEditorMode] = useState<'definition' | 'template'>('definition');
|
||||
const [editorInitialData, setEditorInitialData] = useState<Automation | AutomationTemplateDB | null>(null);
|
||||
|
||||
// Edit existing definition
|
||||
const handleEditClick = async (automation: Automation) => {
|
||||
const fullAutomation = await fetchAutomationById(automation.id);
|
||||
setEditorInitialData(fullAutomation);
|
||||
setEditorMode('definition');
|
||||
setShowEditor(true);
|
||||
};
|
||||
|
||||
// Create from template
|
||||
const handleCreateFromTemplate = async (template: AutomationTemplateDB) => {
|
||||
// Extract label in user's language
|
||||
const userLang = getUserLanguage(); // e.g., 'de' or 'en'
|
||||
const label = template.label[userLang] || template.label.en || 'New Automation';
|
||||
|
||||
// Pre-fill new definition
|
||||
const newDefinition: Partial<Automation> = {
|
||||
label: label,
|
||||
template: template.template,
|
||||
placeholders: {},
|
||||
schedule: '0 22 * * *',
|
||||
active: false,
|
||||
mandateId: mandateId,
|
||||
featureInstanceId: featureInstanceId,
|
||||
};
|
||||
|
||||
setEditorInitialData(newDefinition as Automation);
|
||||
setEditorMode('definition');
|
||||
setShowEditor(true);
|
||||
};
|
||||
|
||||
// Save handler from editor
|
||||
const handleEditorSave = async (data: any) => {
|
||||
if (editorMode === 'definition') {
|
||||
if (editorInitialData?.id) {
|
||||
await handleAutomationUpdate(editorInitialData.id, data);
|
||||
} else {
|
||||
await handleAutomationCreate(data);
|
||||
}
|
||||
}
|
||||
// Template save is handled in AutomationTemplatesPage
|
||||
setShowEditor(false);
|
||||
refetch();
|
||||
};
|
||||
```
|
||||
|
||||
### 7.2 Template-Auswahl Modal anpassen
|
||||
|
||||
Bisherige Template-Auswahl nutzt jetzt `AutomationTemplateDB` statt altes Format:
|
||||
|
||||
```typescript
|
||||
const [templatesDB, setTemplatesDB] = useState<AutomationTemplateDB[]>([]);
|
||||
|
||||
const loadTemplates = async () => {
|
||||
setLoadingTemplates(true);
|
||||
const data = await fetchAutomationTemplatesDB(request);
|
||||
setTemplatesDB(data);
|
||||
setLoadingTemplates(false);
|
||||
};
|
||||
|
||||
// In Template-Modal: Display label in current language
|
||||
// Nutze inline-Zugriff oder FormGeneratorTable für Liste
|
||||
const { currentLanguage } = useLanguage();
|
||||
const getLang = (ml: TextMultilingual | undefined) =>
|
||||
ml?.[currentLanguage as keyof TextMultilingual] || ml?.en || '';
|
||||
|
||||
{templatesDB.map(template => (
|
||||
<div key={template.id} onClick={() => handleCreateFromTemplate(template)}>
|
||||
<h4>{getLang(template.label)}</h4>
|
||||
<p>{getLang(template.overview)}</p>
|
||||
</div>
|
||||
))}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Navigation / Routing
|
||||
|
||||
### Architektur-Hinweis: ObjectKey-Formate
|
||||
|
||||
| Kontext | Format | Beispiel | Verwendung |
|
||||
|---------|--------|----------|------------|
|
||||
| Backend Navigation | `ui.system.xxx` | `ui.system.automations` | NAVIGATION_SECTIONS in mainSystem.py |
|
||||
| Frontend Icons | `page.system.xxx` | `page.system.automations` | pageRegistry.tsx PAGE_ICONS |
|
||||
| RBAC DATA | `data.automation.xxx` | `data.automation.AutomationTemplate` | Tabellen-Zugriffsregeln |
|
||||
| RBAC UI | `ui.feature.automation.xxx` | `ui.feature.automation.templates` | Feature-UI-Objekte |
|
||||
|
||||
Die Sidebar-Navigation kommt vom **Backend** (`/api/navigation`), nicht aus pageRegistry. PageRegistry liefert nur die **Icons** zu den vom Backend gelieferten Items.
|
||||
|
||||
---
|
||||
|
||||
### 8.1 Backend: mainSystem.py (NAVIGATION_SECTIONS)
|
||||
|
||||
**WICHTIG:** Die Navigation wird vom Backend gesteuert! In `gateway/modules/system/mainSystem.py` unter `NAVIGATION_SECTIONS` im Abschnitt "workflows" hinzufügen:
|
||||
|
||||
```python
|
||||
{
|
||||
"id": "automation-templates",
|
||||
"objectKey": "ui.system.automation-templates",
|
||||
"label": {"en": "Templates", "de": "Vorlagen", "fr": "Modèles"},
|
||||
"icon": "FaFileAlt", # Oder FaCopy für Vorlagen
|
||||
"path": "/workflows/automation-templates",
|
||||
"order": 35, # Nach automations (30)
|
||||
"public": True,
|
||||
},
|
||||
```
|
||||
|
||||
### 8.2 Frontend: App.tsx
|
||||
|
||||
Route innerhalb des `<Route path="workflows">` Blocks hinzufügen:
|
||||
|
||||
```typescript
|
||||
// In App.tsx, im workflows-Block:
|
||||
<Route path="workflows">
|
||||
<Route path="playground" element={<PlaygroundPage />} />
|
||||
<Route path="list" element={<WorkflowsPage />} />
|
||||
<Route path="automations" element={<AutomationsPage />} />
|
||||
<Route path="automation-templates" element={<AutomationTemplatesPage />} /> {/* NEU */}
|
||||
</Route>
|
||||
```
|
||||
|
||||
**Import hinzufügen:**
|
||||
```typescript
|
||||
import { AutomationTemplatesPage } from './pages/workflows/AutomationTemplatesPage';
|
||||
```
|
||||
|
||||
### 8.3 Frontend: pageRegistry.tsx
|
||||
|
||||
**WICHTIG:** Format ist `page.system.xxx`, nicht `ui.system.xxx`!
|
||||
|
||||
```typescript
|
||||
// Import hinzufügen (FaFileAlt ist bereits importiert):
|
||||
// import { ..., FaFileAlt, ... } from 'react-icons/fa';
|
||||
|
||||
// In PAGE_ICONS hinzufügen:
|
||||
'page.system.automation-templates': <FaFileAlt />,
|
||||
```
|
||||
|
||||
### 8.4 Frontend: pages/workflows/index.ts
|
||||
|
||||
Export hinzufügen:
|
||||
```typescript
|
||||
export { PlaygroundPage } from './PlaygroundPage';
|
||||
export { WorkflowsPage } from './WorkflowsPage';
|
||||
export { AutomationsPage } from './AutomationsPage';
|
||||
export { AutomationTemplatesPage } from './AutomationTemplatesPage'; // NEU
|
||||
```
|
||||
|
||||
### 8.5 Locales (de.ts, en.ts, fr.ts)
|
||||
|
||||
Übersetzungen hinzufügen:
|
||||
|
||||
**de.ts:**
|
||||
```typescript
|
||||
'nav.automation-templates': 'Vorlagen',
|
||||
'automation-templates.title': 'Automation-Vorlagen',
|
||||
'automation-templates.create': 'Neue Vorlage',
|
||||
'automation-templates.edit': 'Vorlage bearbeiten',
|
||||
'automation-templates.empty': 'Keine Vorlagen vorhanden',
|
||||
```
|
||||
|
||||
**en.ts:**
|
||||
```typescript
|
||||
'nav.automation-templates': 'Templates',
|
||||
'automation-templates.title': 'Automation Templates',
|
||||
'automation-templates.create': 'New Template',
|
||||
'automation-templates.edit': 'Edit Template',
|
||||
'automation-templates.empty': 'No templates available',
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Hilfsfunktionen
|
||||
|
||||
> **Hinweis:** `getMultilingualText()` ist NICHT nötig - FormGeneratorTable rendert TextMultilingual automatisch in der User-Sprache via `formatTextMultilingual()` intern.
|
||||
|
||||
### 9.1 extractPlaceholdersFromJson
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Extract {{KEY:name}} placeholders from JSON string
|
||||
*/
|
||||
export function extractPlaceholdersFromJson(jsonString: string): string[] {
|
||||
const regex = /\{\{KEY:(\w+)\}\}/g;
|
||||
const keys: string[] = [];
|
||||
let match;
|
||||
while ((match = regex.exec(jsonString)) !== null) {
|
||||
if (!keys.includes(match[1])) {
|
||||
keys.push(match[1]);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Zusammenfassung der Dateien
|
||||
|
||||
| Datei | Änderung |
|
||||
|-------|----------|
|
||||
| `api/automationApi.ts` | Neue Typen + API-Funktionen für Templates (DB) und Actions |
|
||||
| `hooks/useAutomations.ts` | Neue Hooks: `useAutomationTemplates`, `useWorkflowActions` |
|
||||
| `pages/workflows/AutomationsPage.tsx` | Editor-Integration, Template-Auswahl mit DB-Templates |
|
||||
| `pages/workflows/AutomationTemplatesPage.tsx` | **Neu**: CRUD für eigene Templates |
|
||||
| `components/AutomationEditor/AutomationEditor.tsx` | **Neu**: Editor mit Modus-Flag |
|
||||
| `components/AutomationEditor/ActionsPanel.tsx` | **Neu**: Actions-Katalog |
|
||||
| `App.tsx` | Neue Route für AutomationTemplatesPage |
|
||||
| `config/pageRegistry.tsx` | Icon für neue Seite |
|
||||
|
||||
---
|
||||
|
||||
## 11. Implementierungsreihenfolge
|
||||
|
||||
1. **API-Typen und Funktionen** (`automationApi.ts`)
|
||||
2. **Hooks** (`useAutomations.ts` erweitern)
|
||||
3. **ActionsPanel** (unabhängig testbar)
|
||||
4. **AutomationEditor** (Kern-Komponente)
|
||||
5. **AutomationTemplatesPage** (CRUD für Templates)
|
||||
6. **AutomationsPage anpassen** (Editor-Integration)
|
||||
7. **Routing** (App.tsx, Navigation)
|
||||
466
implementation/doc_automation_templates_impl_gateway.md
Normal file
466
implementation/doc_automation_templates_impl_gateway.md
Normal file
|
|
@ -0,0 +1,466 @@
|
|||
# Automation Templates – Gateway Implementation Concept
|
||||
|
||||
**Basis:** `doc_automation_templates_db_and_editor_concept.md`
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ STATUS: BEREITS IMPLEMENTIERT
|
||||
|
||||
**Das Backend ist vollständig implementiert!** Folgende Komponenten existieren bereits:
|
||||
|
||||
| Komponente | Status | Datei |
|
||||
|------------|--------|-------|
|
||||
| AutomationTemplate Model | ✅ Vorhanden | `datamodelFeatureAutomation.py:49` |
|
||||
| RBAC Namespace | ✅ Vorhanden | `interfaceRbac.py:69` |
|
||||
| API Routes (CRUD) | ✅ Vorhanden | `routeFeatureAutomation.py:391-607` |
|
||||
| Interface Methods | ✅ Vorhanden | `interfaceFeatureAutomation.py:411-575` |
|
||||
| Navigation Entry | ✅ Vorhanden | `mainSystem.py:96-103` |
|
||||
|
||||
**Was noch fehlt:**
|
||||
- Bootstrap-Funktion für initiale Template-Migration (optional)
|
||||
|
||||
**Actions-Katalog:** ✅ Bereits vorhanden unter `GET /api/automations/actions` (Zeile 293)
|
||||
|
||||
---
|
||||
|
||||
## 1. Bestehendes Datenmodell: AutomationTemplate
|
||||
|
||||
### 1.1 Datei: `gateway/modules/features/automation/datamodelFeatureAutomation.py`
|
||||
|
||||
Neues Modell **AutomationTemplate** hinzufügen (neben bestehendem `AutomationDefinition`):
|
||||
|
||||
```python
|
||||
from modules.datamodels.datamodelUtils import TextMultilingual
|
||||
|
||||
class AutomationTemplate(BaseModel):
|
||||
"""Automation-Vorlage ohne scharfe Placeholder-Werte."""
|
||||
id: str = Field(
|
||||
default_factory=lambda: str(uuid.uuid4()),
|
||||
description="Primary key",
|
||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True}
|
||||
)
|
||||
label: TextMultilingual = Field(
|
||||
description="Template name (multilingual)",
|
||||
json_schema_extra={"frontend_type": "multilingual", "frontend_required": True}
|
||||
)
|
||||
overview: Optional[TextMultilingual] = Field(
|
||||
None,
|
||||
description="Short description (multilingual)",
|
||||
json_schema_extra={"frontend_type": "multilingual", "frontend_required": False}
|
||||
)
|
||||
template: str = Field(
|
||||
description="JSON workflow structure with {{KEY:...}} placeholders",
|
||||
json_schema_extra={"frontend_type": "textarea", "frontend_required": True}
|
||||
)
|
||||
# System fields (_createdAt, _createdBy, etc.) werden automatisch vom DB-Connector gesetzt
|
||||
|
||||
|
||||
registerModelLabels(
|
||||
"AutomationTemplate",
|
||||
{"en": "Automation Template", "de": "Automation-Vorlage", "fr": "Modèle d'automatisation"},
|
||||
{
|
||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
||||
"label": {"en": "Label", "de": "Bezeichnung", "fr": "Libellé"},
|
||||
"overview": {"en": "Overview", "de": "Übersicht", "fr": "Aperçu"},
|
||||
"template": {"en": "Template", "de": "Vorlage", "fr": "Modèle"},
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### 1.2 Namespace & Tabelle
|
||||
|
||||
- **Namespace:** `data.automation` (wie AutomationDefinition)
|
||||
- **Tabelle:** `AutomationTemplate`
|
||||
- Registrierung in `interfaceRbac.py` (TABLE_NAMESPACE_MAP):
|
||||
|
||||
```python
|
||||
"AutomationTemplate": "automation",
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. RBAC für AutomationTemplate
|
||||
|
||||
### 2.1 Bootstrap-Regeln (interfaceBootstrap.py)
|
||||
|
||||
In `_createTableSpecificRules()` hinzufügen:
|
||||
|
||||
```python
|
||||
# AutomationTemplate: MY-level (user-owned), like AutomationDefinition
|
||||
for roleId in [adminId, userId]:
|
||||
if roleId:
|
||||
tableRules.append(AccessRule(
|
||||
roleId=roleId,
|
||||
context=AccessRuleContext.DATA,
|
||||
item="data.automation.AutomationTemplate",
|
||||
view=True,
|
||||
read=AccessLevel.MY,
|
||||
create=AccessLevel.MY,
|
||||
update=AccessLevel.MY,
|
||||
delete=AccessLevel.MY,
|
||||
))
|
||||
if viewerId:
|
||||
tableRules.append(AccessRule(
|
||||
roleId=viewerId,
|
||||
context=AccessRuleContext.DATA,
|
||||
item="data.automation.AutomationTemplate",
|
||||
view=True,
|
||||
read=AccessLevel.MY,
|
||||
create=AccessLevel.NONE,
|
||||
update=AccessLevel.NONE,
|
||||
delete=AccessLevel.NONE,
|
||||
))
|
||||
```
|
||||
|
||||
In `_ensureDataContextRules()` (Zeile ~845) ergänzen:
|
||||
|
||||
```python
|
||||
"data.automation.AutomationTemplate",
|
||||
```
|
||||
|
||||
### 2.2 UI-Regeln
|
||||
|
||||
In `mainAutomation.py` bereits vorhanden:
|
||||
```python
|
||||
{"objectKey": "ui.feature.automation.templates", ...}
|
||||
```
|
||||
Sichtbarkeit gemäss RBAC (alle mit Berechtigung; nicht nur SysAdmin).
|
||||
|
||||
### 2.3 Navigation (mainSystem.py)
|
||||
|
||||
In `NAVIGATION_SECTIONS` unter dem "workflows" Abschnitt hinzufügen:
|
||||
|
||||
```python
|
||||
{
|
||||
"id": "automation-templates",
|
||||
"objectKey": "ui.system.automation-templates",
|
||||
"label": {"en": "Templates", "de": "Vorlagen", "fr": "Modèles"},
|
||||
"icon": "FaFileAlt",
|
||||
"path": "/workflows/automation-templates",
|
||||
"order": 35, # Nach automations (30)
|
||||
"public": True,
|
||||
},
|
||||
```
|
||||
|
||||
**WICHTIG:** Das `objectKey` Format für Navigation ist `ui.system.xxx`, nicht `page.system.xxx`!
|
||||
|
||||
---
|
||||
|
||||
## 3. API Routes für AutomationTemplate
|
||||
|
||||
### 3.1 Neue Route-Datei oder erweitern: `routeFeatureAutomation.py`
|
||||
|
||||
Endpoints unter `/api/automation-templates` (oder `/api/automations/templates` erweitern):
|
||||
|
||||
```python
|
||||
from modules.features.automation.datamodelFeatureAutomation import AutomationTemplate
|
||||
|
||||
# GET /api/automation-templates - Liste (RBAC-gefiltert)
|
||||
@router.get("/templates", response_model=PaginatedResponse[AutomationTemplate])
|
||||
async def get_templates(
|
||||
request: Request,
|
||||
pagination: Optional[str] = Query(None),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
):
|
||||
"""Get automation templates, filtered by RBAC (MY = own templates)."""
|
||||
chatInterface = getChatInterface(context.user, ...)
|
||||
result = chatInterface.getAllAutomationTemplates(pagination=paginationParams)
|
||||
return JSONResponse(content=result)
|
||||
|
||||
# GET /api/automation-templates/{id}
|
||||
@router.get("/templates/{templateId}", response_model=AutomationTemplate)
|
||||
async def get_template(templateId: str, context: RequestContext = Depends(getRequestContext)):
|
||||
chatInterface = getChatInterface(context.user, ...)
|
||||
template = chatInterface.getAutomationTemplate(templateId)
|
||||
if not template:
|
||||
raise HTTPException(404, "Template not found")
|
||||
return template
|
||||
|
||||
# POST /api/automation-templates
|
||||
@router.post("/templates", response_model=AutomationTemplate)
|
||||
async def create_template(
|
||||
request: Request,
|
||||
templateData: Dict[str, Any] = Body(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
):
|
||||
chatInterface = getChatInterface(context.user, ...)
|
||||
return chatInterface.createAutomationTemplate(templateData)
|
||||
|
||||
# PUT /api/automation-templates/{id}
|
||||
@router.put("/templates/{templateId}", response_model=AutomationTemplate)
|
||||
async def update_template(
|
||||
templateId: str,
|
||||
templateData: Dict[str, Any] = Body(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
):
|
||||
chatInterface = getChatInterface(context.user, ...)
|
||||
return chatInterface.updateAutomationTemplate(templateId, templateData)
|
||||
|
||||
# DELETE /api/automation-templates/{id}
|
||||
@router.delete("/templates/{templateId}")
|
||||
async def delete_template(templateId: str, context: RequestContext = Depends(getRequestContext)):
|
||||
chatInterface = getChatInterface(context.user, ...)
|
||||
success = chatInterface.deleteAutomationTemplate(templateId)
|
||||
if not success:
|
||||
raise HTTPException(404, "Template not found or no permission")
|
||||
return {"success": True}
|
||||
```
|
||||
|
||||
### 3.2 Interface-Methoden (interfaceDbChat.py)
|
||||
|
||||
Analog zu `getAllAutomationDefinitions`, `createAutomationDefinition`, etc.:
|
||||
|
||||
```python
|
||||
def getAllAutomationTemplates(self, pagination=None) -> Union[List[Dict], PaginatedResult]:
|
||||
"""Returns templates filtered by RBAC (MY = own templates)."""
|
||||
filteredTemplates = getRecordsetWithRBAC(self.db, AutomationTemplate, self.currentUser)
|
||||
# ... pagination, enrichment
|
||||
return filteredTemplates
|
||||
|
||||
def getAutomationTemplate(self, templateId: str) -> Optional[AutomationTemplate]:
|
||||
filtered = getRecordsetWithRBAC(self.db, AutomationTemplate, self.currentUser, recordFilter={"id": templateId})
|
||||
return AutomationTemplate(**filtered[0]) if filtered else None
|
||||
|
||||
def createAutomationTemplate(self, templateData: Dict) -> AutomationTemplate:
|
||||
if not self.checkRbacPermission(AutomationTemplate, "create"):
|
||||
raise ValueError("No permission to create template")
|
||||
simpleFields, _ = self._separateObjectFields(AutomationTemplate, templateData)
|
||||
created = self.db.recordCreate(AutomationTemplate, simpleFields)
|
||||
return AutomationTemplate(**created)
|
||||
|
||||
def updateAutomationTemplate(self, templateId: str, templateData: Dict) -> AutomationTemplate:
|
||||
existing = self.getAutomationTemplate(templateId)
|
||||
if not existing:
|
||||
raise ValueError("Template not found")
|
||||
if not self.checkRbacPermission(AutomationTemplate, "update", templateId):
|
||||
raise ValueError("No permission to update")
|
||||
simpleFields, _ = self._separateObjectFields(AutomationTemplate, templateData)
|
||||
updated = self.db.recordModify(AutomationTemplate, templateId, simpleFields)
|
||||
return AutomationTemplate(**updated)
|
||||
|
||||
def deleteAutomationTemplate(self, templateId: str) -> bool:
|
||||
existing = self.getAutomationTemplate(templateId)
|
||||
if not existing:
|
||||
return False
|
||||
if not self.checkRbacPermission(AutomationTemplate, "delete", templateId):
|
||||
raise ValueError("No permission to delete")
|
||||
self.db.recordDelete(AutomationTemplate, templateId)
|
||||
return True
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Bootstrap: Template-Seed
|
||||
|
||||
### 4.1 Neue Funktion in interfaceBootstrap.py
|
||||
|
||||
```python
|
||||
def initAutomationTemplates(db: DatabaseConnector) -> None:
|
||||
"""
|
||||
Seed initial automation templates from subAutomationTemplates.py.
|
||||
Only runs if no templates exist yet (bootstrap).
|
||||
Creates templates with _createdBy = admin user (SysAdmin privilege).
|
||||
"""
|
||||
from modules.features.automation.subAutomationTemplates import AUTOMATION_TEMPLATES
|
||||
from modules.features.automation.datamodelFeatureAutomation import AutomationTemplate
|
||||
|
||||
# Check if templates already exist
|
||||
existing = db.getRecordset(AutomationTemplate)
|
||||
if existing:
|
||||
logger.info(f"Automation templates already seeded ({len(existing)} templates)")
|
||||
return
|
||||
|
||||
# Get admin user ID for _createdBy
|
||||
adminUsers = db.getRecordset(UserInDB, {"email": APP_CONFIG.ADMIN_EMAIL})
|
||||
adminUserId = adminUsers[0]["id"] if adminUsers else None
|
||||
|
||||
templates = AUTOMATION_TEMPLATES.get("sets", [])
|
||||
for i, templateSet in enumerate(templates):
|
||||
templateContent = templateSet.get("template", {})
|
||||
overview = templateContent.get("overview", f"Template {i+1}")
|
||||
|
||||
# Create multilingual label from overview (German as primary since current templates are German)
|
||||
label = {"en": overview, "de": overview}
|
||||
|
||||
# Create template WITHOUT parameters (no sharp values)
|
||||
templateData = {
|
||||
"label": label,
|
||||
"overview": {"en": overview, "de": overview},
|
||||
"template": json.dumps(templateContent), # Only template JSON with {{KEY:...}}
|
||||
}
|
||||
|
||||
# Set _createdBy to admin for bootstrap
|
||||
if adminUserId:
|
||||
templateData["_createdBy"] = adminUserId
|
||||
|
||||
db.recordCreate(AutomationTemplate, templateData)
|
||||
logger.info(f"Created automation template: {overview}")
|
||||
|
||||
logger.info(f"Seeded {len(templates)} automation templates")
|
||||
```
|
||||
|
||||
### 4.2 In initBootstrap() aufrufen
|
||||
|
||||
```python
|
||||
def initBootstrap(db: DatabaseConnector) -> None:
|
||||
# ... existing code ...
|
||||
|
||||
# Seed automation templates (after admin user exists)
|
||||
initAutomationTemplates(db)
|
||||
|
||||
logger.info("System bootstrap completed")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Actions-Katalog Endpoint
|
||||
|
||||
### 5.1 Neuer Endpoint: GET /api/automations/actions
|
||||
|
||||
```python
|
||||
from modules.workflows.processing.shared.methodDiscovery import discoverMethods, methods
|
||||
|
||||
@router.get("/actions")
|
||||
async def get_available_actions(
|
||||
request: Request,
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
):
|
||||
"""
|
||||
Get available workflow actions filtered by RBAC.
|
||||
Returns action definitions with parameters and example JSON snippets.
|
||||
"""
|
||||
# Ensure methods are discovered
|
||||
if not methods:
|
||||
# Need a serviceCenter with current user for RBAC filtering
|
||||
# This requires a lightweight serviceCenter or direct method iteration
|
||||
pass
|
||||
|
||||
actionsList = []
|
||||
for methodName, methodInfo in methods.items():
|
||||
# Skip duplicate short names (e.g., "ai" and "AiMethod" are same)
|
||||
if methodName != methodName.lower():
|
||||
continue
|
||||
|
||||
methodInstance = methodInfo.get("instance")
|
||||
if not methodInstance:
|
||||
continue
|
||||
|
||||
for actionName, actionDef in methodInstance._actions.items():
|
||||
actionId = actionDef.actionId
|
||||
|
||||
# RBAC check: user needs view permission on this action (RESOURCE context)
|
||||
permissions = context.rbac.getUserPermissions(
|
||||
user=context.user,
|
||||
context=AccessRuleContext.RESOURCE,
|
||||
item=actionId
|
||||
)
|
||||
if not permissions.view:
|
||||
continue
|
||||
|
||||
# Build action info from WorkflowActionDefinition
|
||||
actionInfo = {
|
||||
"method": methodName,
|
||||
"action": actionName,
|
||||
"actionId": actionId,
|
||||
"description": actionDef.description,
|
||||
"category": actionDef.category,
|
||||
"parameters": []
|
||||
}
|
||||
|
||||
# Add parameters from WorkflowActionParameter
|
||||
for paramName, paramDef in actionDef.parameters.items():
|
||||
actionInfo["parameters"].append({
|
||||
"name": paramName,
|
||||
"type": paramDef.type,
|
||||
"frontendType": paramDef.frontendType.value if paramDef.frontendType else "text",
|
||||
"required": paramDef.required,
|
||||
"default": paramDef.default,
|
||||
"description": paramDef.description,
|
||||
"frontendOptions": paramDef.frontendOptions,
|
||||
})
|
||||
|
||||
# Build example JSON snippet for copy/paste
|
||||
exampleParams = {}
|
||||
for paramName, paramDef in actionDef.parameters.items():
|
||||
if paramDef.required:
|
||||
exampleParams[paramName] = f"{{{{KEY:{paramName}}}}}"
|
||||
else:
|
||||
exampleParams[paramName] = paramDef.default or f"{{{{KEY:{paramName}}}}}"
|
||||
|
||||
actionInfo["exampleJson"] = {
|
||||
"execMethod": methodName,
|
||||
"execAction": actionName,
|
||||
"execParameters": exampleParams,
|
||||
"execResultLabel": f"{methodName}_{actionName}_result"
|
||||
}
|
||||
|
||||
actionsList.append(actionInfo)
|
||||
|
||||
return JSONResponse(content={"actions": actionsList})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Label in User-Sprache bei Definition-Erstellung
|
||||
|
||||
### 6.1 Bei "Aus Template erstellen" (Frontend ruft Backend)
|
||||
|
||||
Wenn das Frontend eine neue AutomationDefinition aus einem Template erstellt, sendet es:
|
||||
- `template` (JSON von AutomationTemplate.template)
|
||||
- `label` (aus AutomationTemplate.label in User-Sprache extrahiert)
|
||||
|
||||
**Backend-seitig** (falls Backend den Label-Extract macht):
|
||||
|
||||
```python
|
||||
def createAutomationFromTemplate(self, templateId: str, userLanguage: str = "en") -> AutomationDefinition:
|
||||
"""Create a new AutomationDefinition from a template, label in user's language."""
|
||||
template = self.getAutomationTemplate(templateId)
|
||||
if not template:
|
||||
raise ValueError("Template not found")
|
||||
|
||||
# Extract label in user's language
|
||||
labelMulti = template.label # TextMultilingual object
|
||||
if hasattr(labelMulti, 'get_text'):
|
||||
label = labelMulti.get_text(userLanguage)
|
||||
elif isinstance(labelMulti, dict):
|
||||
label = labelMulti.get(userLanguage) or labelMulti.get("en", "New Automation")
|
||||
else:
|
||||
label = str(labelMulti)
|
||||
|
||||
# Create definition with template content
|
||||
definitionData = {
|
||||
"label": label,
|
||||
"template": template.template, # Copy template JSON
|
||||
"placeholders": {}, # Empty - user fills in later
|
||||
"schedule": "0 22 * * *", # Default schedule
|
||||
"active": False,
|
||||
}
|
||||
|
||||
return self.createAutomationDefinition(definitionData)
|
||||
```
|
||||
|
||||
**Alternative:** Frontend extrahiert Label selbst und sendet direkt an `createAutomationDefinition`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Zusammenfassung der Änderungen
|
||||
|
||||
| Datei | Änderung |
|
||||
|-------|----------|
|
||||
| `datamodelFeatureAutomation.py` | Neues Modell `AutomationTemplate` mit `TextMultilingual` für label/overview |
|
||||
| `interfaceRbac.py` | TABLE_NAMESPACE_MAP erweitern: `"AutomationTemplate": "automation"` |
|
||||
| `interfaceBootstrap.py` | RBAC-Regeln für AutomationTemplate (MY); Bootstrap-Seed `initAutomationTemplates()` |
|
||||
| `interfaceDbChat.py` | CRUD-Methoden für AutomationTemplate (analog AutomationDefinition) |
|
||||
| `routeFeatureAutomation.py` | Endpoints: GET/POST/PUT/DELETE `/api/automation-templates/*`, GET `/api/automations/actions` |
|
||||
| `mainAutomation.py` | UI-Object `ui.feature.automation.templates` bereits vorhanden |
|
||||
|
||||
---
|
||||
|
||||
## 8. Abhängigkeiten & Reihenfolge
|
||||
|
||||
1. **Datamodel** erstellen (AutomationTemplate)
|
||||
2. **RBAC** in interfaceRbac.py und interfaceBootstrap.py
|
||||
3. **Interface-Methoden** in interfaceDbChat.py
|
||||
4. **Routes** in routeFeatureAutomation.py
|
||||
5. **Bootstrap-Seed** in interfaceBootstrap.py (nach DB-Migration)
|
||||
6. **Actions-Endpoint** als letzte Erweiterung
|
||||
|
|
@ -147,7 +147,7 @@ async def _callWithContentParts(self, request: AiCallRequest) -> AiCallResponse:
|
|||
return AiCallResponse(
|
||||
content=mergedContent,
|
||||
modelName="multiple",
|
||||
priceUsd=sum(r.priceUsd for r in allResults),
|
||||
priceCHF=sum(r.priceCHF for r in allResults),
|
||||
processingTime=sum(r.processingTime for r in allResults),
|
||||
bytesSent=sum(r.bytesSent for r in allResults),
|
||||
bytesReceived=sum(r.bytesReceived for r in allResults),
|
||||
|
|
@ -185,7 +185,7 @@ async def _processContentPartWithFallback(self, contentPart: ContentPart, prompt
|
|||
|
||||
# Merge chunk results
|
||||
mergedContent = self._mergeChunkResults(chunkResults)
|
||||
totalPrice = sum(r.priceUsd for r in chunkResults)
|
||||
totalPrice = sum(r.priceCHF for r in chunkResults)
|
||||
totalTime = sum(r.processingTime for r in chunkResults)
|
||||
totalBytesSent = sum(r.bytesSent for r in chunkResults)
|
||||
totalBytesReceived = sum(r.bytesReceived for r in chunkResults)
|
||||
|
|
@ -195,7 +195,7 @@ async def _processContentPartWithFallback(self, contentPart: ContentPart, prompt
|
|||
return AiCallResponse(
|
||||
content=mergedContent,
|
||||
modelName=model.name,
|
||||
priceUsd=totalPrice,
|
||||
priceCHF=totalPrice,
|
||||
processingTime=totalTime,
|
||||
bytesSent=totalBytesSent,
|
||||
bytesReceived=totalBytesReceived,
|
||||
|
|
@ -316,7 +316,7 @@ async def _processPartsWithMapping(self, extractionResult: List[ContentExtracted
|
|||
"resultSize": len(response.content),
|
||||
"typeGroup": part.typeGroup,
|
||||
"modelName": response.modelName,
|
||||
"priceUsd": response.priceUsd
|
||||
"priceCHF": response.priceCHF
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ Owner: Workflow/Data Layer
|
|||
- Example values: "action.outlook.readMails", "ai.process.document.name"
|
||||
- Add: engine: str
|
||||
- Example values: "ai.anthropic.35", "ai.tavily.basic", "renderer.docx"
|
||||
- Add: priceUsd: float (calculated price in USD for the operation)
|
||||
- Add: priceCHF: float (calculated price in USD for the operation)
|
||||
|
||||
- ChatWorkflow
|
||||
- stats: change from Optional[ChatStat] to List[ChatStat] (default [])
|
||||
|
|
@ -52,7 +52,7 @@ DB logic:
|
|||
## Service Layer Changes (modules/services/serviceWorkflow/mainServiceWorkflow.py)
|
||||
|
||||
- Replace storeWorkflowStat(workflow, statData) to:
|
||||
- Coerce statData to ChatStat fields (process, engine, priceUsd, bytesSent, bytesReceived, processingTime, errorCount) --> to handover pydantic model (with id empty), not the single values
|
||||
- Coerce statData to ChatStat fields (process, engine, priceCHF, bytesSent, bytesReceived, processingTime, errorCount) --> to handover pydantic model (with id empty), not the single values
|
||||
- Set id and workflowId
|
||||
- Persist via interfaceDbChat.createWorkflowStat
|
||||
- Append to workflow.stats in memory
|
||||
|
|
@ -65,10 +65,10 @@ DB logic:
|
|||
## Instrumentation Points (where to emit stats)
|
||||
|
||||
1) AI Calls (modules/interfaces/interfaceAiObjects.py)
|
||||
- Each AI call measures processing time, calculates priceUsd, and tracks bytes sent/received
|
||||
- Returns standardized AiCallResponse with priceUsd, processingTime, bytesSent, bytesReceived, errorCount included (to adapt pydantic model)
|
||||
- Each AI call measures processing time, calculates priceCHF, and tracks bytes sent/received
|
||||
- Returns standardized AiCallResponse with priceCHF, processingTime, bytesSent, bytesReceived, errorCount included (to adapt pydantic model)
|
||||
- All AI functions (call, callImage, generateImage, webQuery, etc.) return this standardized response
|
||||
- Model-specific pricing functions calculate priceUsd based on (processingTime, bytesSent, bytesReceived)
|
||||
- Model-specific pricing functions calculate priceCHF based on (processingTime, bytesSent, bytesReceived)
|
||||
|
||||
2) Service Layer (modules/services/serviceWorkflow/mainServiceWorkflow.py)
|
||||
- Receives AiCallResponse from AI interface
|
||||
|
|
@ -78,7 +78,7 @@ DB logic:
|
|||
3) Extraction/Generation Services
|
||||
- services/serviceExtraction/mainServiceExtraction.py and related extractors
|
||||
- services/serviceGeneration/* (document rendering/generation)
|
||||
- Emit ChatStat per completed operation with process, engine, processingTime, bytes, priceUsd, etc.
|
||||
- Emit ChatStat per completed operation with process, engine, processingTime, bytes, priceCHF, etc.
|
||||
|
||||
4) Workflow-level summaries (optional)
|
||||
- At workflow completion, compute a summary stat (aggregated bytes/costs) if desired
|
||||
|
|
@ -90,7 +90,7 @@ DB logic:
|
|||
Update AiCallResponse to include standardized stats fields:
|
||||
|
||||
- Remove: usedTokens, costEstimate
|
||||
- Add: priceUsd: float (calculated price in USD)
|
||||
- Add: priceCHF: float (calculated price in USD)
|
||||
- Add: processingTime: float (duration in seconds)
|
||||
- Add: bytesSent: int (input data size in bytes)
|
||||
- Add: bytesReceived: int (output data size in bytes)
|
||||
|
|
@ -100,7 +100,7 @@ Update AiCallResponse to include standardized stats fields:
|
|||
|
||||
Each AI model should implement a pricing calculation function that takes the operation parameters and returns the cost in USD:
|
||||
|
||||
- Function signature: `_calculatePriceUsd(processingTime: float, bytesSent: int, bytesReceived: int) -> float`
|
||||
- Function signature: `_calculatepriceCHF(processingTime: float, bytesSent: int, bytesReceived: int) -> float`
|
||||
- Parameters:
|
||||
- `processingTime`: Duration of the operation in seconds
|
||||
- `bytesSent`: Size of input data in bytes
|
||||
|
|
@ -136,7 +136,7 @@ Example implementations:
|
|||
### Phase 1: Core Data Model Changes
|
||||
1. **Update ChatStat model** (modules/datamodels/datamodelChat.py)
|
||||
- Remove: successRate, tokenCount, tokenPriceUnit, tokenPriceAmount, messageId
|
||||
- Add: process: str, engine: str, priceUsd: float
|
||||
- Add: process: str, engine: str, priceCHF: float
|
||||
- Keep: id, workflowId, processingTime, bytesSent, bytesReceived, errorCount
|
||||
|
||||
2. **Update ChatWorkflow model** (modules/datamodels/datamodelChat.py)
|
||||
|
|
@ -148,7 +148,7 @@ Example implementations:
|
|||
|
||||
4. **Update AiCallResponse model** (modules/datamodels/datamodelAi.py)
|
||||
- Remove: usedTokens, costEstimate
|
||||
- Add: priceUsd: float, processingTime: float, bytesSent: int, bytesReceived: int, errorCount: int
|
||||
- Add: priceCHF: float, processingTime: float, bytesSent: int, bytesReceived: int, errorCount: int
|
||||
|
||||
### Phase 2: Database Interface Changes
|
||||
5. **Update interfaceDbChatObjects.py**
|
||||
|
|
@ -159,14 +159,14 @@ Example implementations:
|
|||
|
||||
### Phase 3: AI Interface Layer Changes
|
||||
6. **Add model-specific pricing functions** (modules/interfaces/interfaceAiObjects.py)
|
||||
- Add _calculatePriceUsd() method to each model in aiModels registry
|
||||
- Add _calculatepriceCHF() method to each model in aiModels registry
|
||||
- Implement pricing logic for each connector type (OpenAI, Anthropic, Perplexity, Tavily)
|
||||
|
||||
7. **Update all AI call methods** (modules/interfaces/interfaceAiObjects.py)
|
||||
- Add timing measurement (start/end timestamps)
|
||||
- Calculate bytesSent (input data size)
|
||||
- Calculate bytesReceived (output data size)
|
||||
- Call model-specific _calculatePriceUsd()
|
||||
- Call model-specific _calculatepriceCHF()
|
||||
- Return standardized AiCallResponse with all stats fields
|
||||
|
||||
8. **Update specific AI methods:**
|
||||
|
|
@ -207,7 +207,7 @@ Example implementations:
|
|||
- Update UI polling to handle stats array
|
||||
|
||||
15. **Add comprehensive logging**
|
||||
- Debug logs for each stat emission (process, engine, bytes, priceUsd)
|
||||
- Debug logs for each stat emission (process, engine, bytes, priceCHF)
|
||||
- Error handling for stats emission failures
|
||||
|
||||
### Phase 7: Cleanup and Validation
|
||||
|
|
@ -231,7 +231,7 @@ Example implementations:
|
|||
|
||||
- **Centralized measurement**: AI interface layer handles all timing and byte counting, ensuring consistency.
|
||||
- **Standardized responses**: All AI calls return AiCallResponse with complete stats data.
|
||||
- **Model-specific pricing**: Each model implements `_calculatePriceUsd()` with its own pricing strategy.
|
||||
- **Model-specific pricing**: Each model implements `_calculatepriceCHF()` with its own pricing strategy.
|
||||
- **Service layer simplicity**: Service layer just creates ChatStat objects from AiCallResponse data.
|
||||
- **Append-only stats**: Simplifies auditing and invoicing; no implicit overwrites.
|
||||
- **Workflow-scoped stats**: All stats attached to workflow, not messages; UI gets complete history.
|
||||
|
|
|
|||
Loading…
Reference in a new issue