From 2596bd60c06974933b0c1067175565e3a976f21b Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Sun, 19 Apr 2026 00:03:58 +0200
Subject: [PATCH] fixed proper splitting sysadmin/platformadmin and proper
logic for mandate name(slug) and label(user)
---
TOPICS.md | 1 +
b-reference/platform/mandate.md | 96 ++++
b-reference/platform/rbac.md | 31 +-
c-work/0-ideas/.gitkeep | 0
...-pm-consolidated-customer-requirements.md} | 0
...arch.md => 2026-03-pm-web-image-search.md} | 0
...unified-knowledge-indexing-rag-concept.md} | 0
...pwg-pilot-mietzinsbestaetigung-workflow.md | 404 ++++++++++++++++
.../2026-04-mandate-name-label-logic.md | 184 ++++++++
.../2026-04-sysadmin-authority-split.md | 434 ++++++++++++++++++
10 files changed, 1146 insertions(+), 4 deletions(-)
create mode 100644 b-reference/platform/mandate.md
create mode 100644 c-work/0-ideas/.gitkeep
rename c-work/{1-plan/2026-04-consolidated-customer-requirements.md => 0-ideas/2026-04-pm-consolidated-customer-requirements.md} (100%)
rename c-work/1-plan/{2026-03-web-image-search.md => 2026-03-pm-web-image-search.md} (100%)
rename c-work/1-plan/{2026-04-unified-knowledge-indexing-rag-concept.md => 2026-04-id-unified-knowledge-indexing-rag-concept.md} (100%)
create mode 100644 c-work/1-plan/2026-04-pwg-pilot-mietzinsbestaetigung-workflow.md
create mode 100644 c-work/4-done/2026-04-mandate-name-label-logic.md
create mode 100644 c-work/4-done/2026-04-sysadmin-authority-split.md
diff --git a/TOPICS.md b/TOPICS.md
index 0570598..eb6d6a3 100644
--- a/TOPICS.md
+++ b/TOPICS.md
@@ -33,6 +33,7 @@ Lade immer zuerst diese Datei. Dann gezielt die passende(n) Referenz-Datei(en).
|-------|-------|------------|
| Neutralisierung | b-reference/platform/neutralization.md | Datenschutz, AI-Call-Pipeline, Failsafe |
| RBAC | b-reference/platform/rbac.md | 4-Stufen-Modell, Template-Rollen, Resolution, Datenmodell |
+| Mandate-Identifier | b-reference/platform/mandate.md | `name` (Kurzzeichen) vs. `label` (Voller Name), Slug-Generierung, RBAC-Editierbarkeit, Bootstrap-Migration |
| Datenbank-Architektur | b-reference/platform/database-architecture.md | Interface-Pattern, Connector, Auto-Init, DB-Liste, Database Health, Orphan-Scanner |
| Navigation | b-reference/platform/navigation.md | Menü-Struktur, Admin-Seiten, API |
| Compliance & AI-Audit | b-reference/platform/audit.md | AI-Datenfluss-Log, Security-Audit, Statistiken, RBAC |
diff --git a/b-reference/platform/mandate.md b/b-reference/platform/mandate.md
new file mode 100644
index 0000000..cef41e6
--- /dev/null
+++ b/b-reference/platform/mandate.md
@@ -0,0 +1,96 @@
+
+
+
+
+# Mandate-Identifier: `name` (Kurzzeichen) und `label` (Voller Name)
+
+## Ueberblick
+
+Ein **Mandate** (Mandant / Tenant) hat zwei semantisch getrennte Bezeichner und einen technischen Primaerschluessel:
+
+| Feld | Rolle | Aenderbarkeit | Format |
+| ------- | --------------------------------------------- | -------------------------------------- | ------------------------------- |
+| `id` | Technischer PK / FK-Anker (UUID) | Niemals (auch nicht durch SysAdmin) | UUID v4 |
+| `name` | **Kurzzeichen** — global eindeutiger Slug, technischer Identifier in URLs/Logs/Audit | Nur **PlatformAdmin** oder **SysAdmin** | `^[a-z0-9]+(-[a-z0-9]+)*$`, Laenge 2–32 |
+| `label` | **Voller Name** — Anzeige-Name fuer das UI | **MandateAdmin** und hoeher | non-empty String, beliebige Zeichen |
+
+`id` bleibt der einzige FK-Anker. Saemtliche Tabellen (`Role.mandateId`, `UserMandate.mandateId`, `MandateSubscription.mandateId`, `BillingSettings.mandateId`, …) referenzieren `id`, **nicht** `name`. Rebranding (Label-Wechsel) bricht damit niemals Audit-Trails oder Foreign Keys.
+
+## Warum zwei Felder?
+
+- **Audit-Stabilitaet:** Audit-Reports und Logs zeigen `name` als kompakten, stabilen Code. Wenn ein Kunde sein Anzeige-Label aendert ("Mueller AG" → "Mueller Holding"), bleibt der Code (`mueller-ag`) gleich. Sonst wuerden historische Reports unleserlich werden.
+- **URL-/API-Friendliness:** `name` ist URL-tauglich (lowercase, keine Leerzeichen, keine Sonderzeichen). Zukuenftige Subdomain- oder Pfad-Routing-Features koennen `name` direkt verwenden.
+- **Klare Rollen-Trennung:** MandateAdmin darf sein Label umbenennen, ohne den globalen Identifier zu beeinflussen; nur Plattform-Governance darf den Identifier touchieren.
+
+## Format-Regeln
+
+`name` muss erfuellen:
+
+- nur `a-z`, `0-9`, `-`
+- nicht mit `-` beginnen oder enden
+- keine doppelten Bindestriche
+- Laenge 2–32 Zeichen
+
+`label` muss:
+
+- nicht-leer (nach Trim)
+- darf alle Zeichen enthalten (Umlaute, Akzente, Punkte, Leerzeichen)
+
+## Slug-Generierung (Auto-Allokation)
+
+Wenn `name` bei einem POST nicht angegeben oder leer ist, generiert der Server ihn aus `label`:
+
+1. **Transliteration**: `ae | oe | ue | ss` fuer `ä | ö | ü | ß` (auch Grossvarianten).
+2. **Lowercasen** und alles, was nicht `[a-z0-9]` ist, zu `-` ersetzen.
+3. **Bindestriche kollabieren** und an Raendern trimmen.
+4. **Min-Laenge sichern** (Auffuellen mit `x`).
+5. **Max-Laenge erzwingen** (an letztem Bindestrich ueberhalb der Laengenschranke abschneiden).
+6. **Kollisions-Suffix** `-2`, `-3`, … wenn der Basis-Slug bereits existiert.
+
+Beispiele:
+
+| Label | Erzeugter `name` |
+| ------------------------- | ------------------------ |
+| `Müller AG` | `mueller-ag` |
+| `Home Patrick.Möller` | `home-patrick-moeller` |
+| `Müller AG` (zweites Mal) | `mueller-ag-2` |
+| ` ` (leer) | `mn` (Fallback) |
+
+Code: `gateway/modules/shared/mandateNameUtils.py` (Backend) und `frontend_nyla/src/utils/slugUtils.ts` + `mandateNameUtils.ts` (Frontend, mirror).
+
+## RBAC und Editierbarkeit
+
+| Operation | Rolle | Verhalten |
+| ------------------------- | -------------------------------------- | ------------------------------------------------- |
+| `POST /api/mandates/` | PlatformAdmin | `label` mandatory; `name` optional → Server generiert |
+| `PUT /api/mandates/{id}` `name` | PlatformAdmin oder SysAdmin | Format + Uniqueness validiert (excl. self) |
+| `PUT /api/mandates/{id}` `name` | MandateAdmin | **Ignoriert** (Whitelist `_MANDATE_ADMIN_EDITABLE_FIELDS = {"label"}`) |
+| `PUT /api/mandates/{id}` `label` | MandateAdmin oder hoeher | Pflicht: nicht-leer (nach Trim) |
+| `PUT /api/mandates/{id}` `isSystem` | SysAdmin | Nur SysAdmin |
+| `DELETE /api/mandates/{id}?force=true` | PlatformAdmin (X-Confirm-Name=`name`) | Hard-Delete mit Cascade |
+
+Im UI ist das Kurzzeichen-Feld in den Mandate-Formularen vom Typ `slug` (Live-Maskierung, Auto-Vorschlag aus `label` im Create-Modus, im Edit fuer non-PlatformAdmin disabled).
+
+## Bootstrap-Migration
+
+`interfaceBootstrap._migrateMandateNameLabelSlugRules()` laeuft idempotent beim Boot und bringt Bestandsdaten auf das neue Schema:
+
+1. Wenn `label` leer/None → `label := name`.
+2. Wenn `name` nicht regex-konform → Slug aus `label` generieren mit Kollisions-Suffix.
+3. Stable order ueber `id`-String, deterministisch.
+4. Zweiter Lauf ist No-op.
+
+Tests: `gateway/tests/unit/bootstrap/test_mandateNameMigration.py`.
+
+## UI-Anzeige (Konvention)
+
+- Listen / Auswahlen / Header → `mandateDisplayLabel(m)` = `label || name || id` (Frontend-Util `mandateDisplayUtils.ts`).
+- Detail-Pages mit Identifier-Bezug → `mandateDisplayLineLabelThenSlug(m)` = `Voller Name (kurzzeichen)` wenn beide vorhanden und unterschiedlich.
+- Hard-Delete-Confirm → fragt explizit nach `name` (Kurzzeichen) ab.
+
+## Verwandte Dateien
+
+- Backend: `gateway/modules/datamodels/datamodelUam.py` (Pydantic-Modell), `gateway/modules/interfaces/interfaceDbApp.py` (`createMandate`, `updateMandate`, `_provisionMandateForUser`, `_generateUniqueMandateName`), `gateway/modules/routes/routeDataMandates.py`, `gateway/modules/shared/mandateNameUtils.py`.
+- Frontend: `frontend_nyla/src/types/mandate.ts`, `frontend_nyla/src/api/mandateApi.ts`, `frontend_nyla/src/utils/slugUtils.ts`, `frontend_nyla/src/utils/mandateNameUtils.ts`, `frontend_nyla/src/utils/mandateDisplayUtils.ts`, `frontend_nyla/src/components/FormGenerator/FormGeneratorForm/FormGeneratorForm.tsx` (slug-Type Renderer).
+- Tests: `gateway/tests/unit/shared/test_mandateNameUtils.py`, `gateway/tests/unit/bootstrap/test_mandateNameMigration.py`, `gateway/tests/integration/mandates/`.
+- Plan-Doku: `wiki/c-work/1-plan/2026-04-mandate-name-label-logic.md`.
diff --git a/b-reference/platform/rbac.md b/b-reference/platform/rbac.md
index cbe2c15..3122db6 100644
--- a/b-reference/platform/rbac.md
+++ b/b-reference/platform/rbac.md
@@ -1,6 +1,6 @@
-
-
+
+
# RBAC-System
@@ -8,6 +8,29 @@
Das Role-Based Access Control baut auf vier Stufen auf: **System**, **Mandant**, **Feature** und **Feature-Instanz**. Berechtigungen werden in **Access Rules** pro Rollenlabel und Kontext (DATA, UI, RESOURCE) gebunden. Auswertung erfolgt zentral ueber `interfaceRbac.py`; Zielbild: moeglichst **filternd in SQL** statt vollstaendiger Tabellen-Scans in Python. Nutzer koennen mehrere Rollenlabels gleichzeitig tragen; die effektive Berechtigung entsteht aus **Oeffnungslogik (Union)** ueber alle Rollen.
+## Platform-Governance-Autoritaet (zwei orthogonale Flags)
+
+Neben den Mandanten-scoped Rollen kennt das System zwei **plattformweite User-Flags** auf der `User`-Tabelle. Sie sind **einzeln vergebbar** und decken zwei unabhaengige Autoritaets-Achsen ab:
+
+| Flag | Zweck | RBAC-Bypass | FastAPI-Dependency |
+| --------------------- | -------------------------------------------------------------- | ----------- | ------------------------- |
+| `isSysAdmin` | Infrastruktur-Operator (Logs, Tokens, DB-Health, i18n-Master, global-scoped Files/Sources) | **ja** | `requireSysAdmin` |
+| `isPlatformAdmin` | Cross-Mandate-Governance (User-/Mandate-/RBAC-/Feature-Registry-Verwaltung ueber alle Mandanten) | **nein** | `requirePlatformAdmin` |
+
+Wichtig:
+
+- `isSysAdmin` wirkt als **harter RBAC-Engine-Bypass** in `rbac.py:getUserPermissions`. Reserviert fuer Infrastruktur-Operationen, die unabhaengig vom RBAC-Schema funktionieren muessen.
+- `isPlatformAdmin` gibt **keinen** impliziten Daten-Zugriff. Er erlaubt nur den Zugriff auf Admin-Routen, die explizit `requirePlatformAdmin` deklarieren (z.B. `routeDataMandates`, `routeAdminRbacRules`, `routeBilling`-Cross-Mandate, `routeAdminUserAccessOverview`).
+- Die historische `sysadmin`-Rolle im Root-Mandant wurde **eliminiert**. Eine einmalige idempotente Migration in `interfaceBootstrap._migrateAndDropSysAdminRole()` befoerdert ehemalige Rolleninhaber zu `isPlatformAdmin=True` und entfernt Rolle, AccessRules und `UserMandateRole`-Eintraege.
+
+Beispiel-Profile:
+
+- "Operations-Engineer" → `isSysAdmin=true`, `isPlatformAdmin=false` (kann Logs und DB pruefen, aber keine User/Mandate verwalten).
+- "Customer-Success-Admin" → `isSysAdmin=false`, `isPlatformAdmin=true` (kann Mandanten deblockieren, User-Access-Overview sehen, aber nicht in Server-Logs schauen).
+- "Plattform-Super-Admin" → beide auf `true`.
+
+Details + Migrations-Plan: `wiki/c-work/4-done/2026-04-sysadmin-authority-split.md`.
+
---
## 4-Stufen-Hierarchie
@@ -44,8 +67,8 @@ System (PowerOn Platform)
### Sonderfaelle
-- **SysAdmin**: Eine globale Rolle auf Root-Mandant-Ebene. Wird in `_initSysAdminRole()` beim Bootstrap angelegt -- keine Template-Kopie.
-- **SystemUser / EventUser**: Technische System-Accounts, nicht an echte Benutzer gebunden.
+- **SysAdmin / PlatformAdmin**: Plattformweite Autoritaet wird ueber **User-Flags** geregelt (`isSysAdmin`, `isPlatformAdmin`), nicht ueber Rollen. Siehe Abschnitt "Platform-Governance-Autoritaet".
+- **SystemUser / EventUser**: Technische System-Accounts, nicht an echte Benutzer gebunden. Beide bekommen `isSysAdmin=true`; nur `admin` bekommt zusaetzlich `isPlatformAdmin=true`.
---
diff --git a/c-work/0-ideas/.gitkeep b/c-work/0-ideas/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/c-work/1-plan/2026-04-consolidated-customer-requirements.md b/c-work/0-ideas/2026-04-pm-consolidated-customer-requirements.md
similarity index 100%
rename from c-work/1-plan/2026-04-consolidated-customer-requirements.md
rename to c-work/0-ideas/2026-04-pm-consolidated-customer-requirements.md
diff --git a/c-work/1-plan/2026-03-web-image-search.md b/c-work/1-plan/2026-03-pm-web-image-search.md
similarity index 100%
rename from c-work/1-plan/2026-03-web-image-search.md
rename to c-work/1-plan/2026-03-pm-web-image-search.md
diff --git a/c-work/1-plan/2026-04-unified-knowledge-indexing-rag-concept.md b/c-work/1-plan/2026-04-id-unified-knowledge-indexing-rag-concept.md
similarity index 100%
rename from c-work/1-plan/2026-04-unified-knowledge-indexing-rag-concept.md
rename to c-work/1-plan/2026-04-id-unified-knowledge-indexing-rag-concept.md
diff --git a/c-work/1-plan/2026-04-pwg-pilot-mietzinsbestaetigung-workflow.md b/c-work/1-plan/2026-04-pwg-pilot-mietzinsbestaetigung-workflow.md
new file mode 100644
index 0000000..6a9d0cc
--- /dev/null
+++ b/c-work/1-plan/2026-04-pwg-pilot-mietzinsbestaetigung-workflow.md
@@ -0,0 +1,404 @@
+
+
+
+
+
+# PWG-Pilot: Jahresmietzinsbestätigungs-Workflow + Workflow-File-IO
+
+## Beschreibung und Kontext
+
+**Quelle:** Konsolidierter Kundenwünsche-Plan, Abschnitt 1.9c — PWG-Workshop 16.04.2026.
+
+**Business Case:** PWG verschickt 4 × 800 = 3'200 Jahresmietzinsbestätigungen pro Jahr. Rückantworten (Scans) werden aktuell manuell verarbeitet. Pilot-Ziel: AI-gestützte Verarbeitung mit Antwortvorschlägen, Versand Sommer 2026.
+
+**Dieser Plan deckt drei zusammenhängende Themen ab:**
+
+1. **Workflow-File-IO** — Workflows als File exportieren (in UDB speichern) und aus UDB-File in den Graph-Editor laden. Voraussetzung, damit der Pilot-Workflow als versionierbares Asset im Repo geliefert werden kann.
+2. **Agent-Tools Full-CRUD** — Der AI-Agent erhält im `workflow`-Toolbox die vollständigen Operationen `createWorkflow`, `createWorkflowFromFile`, `exportWorkflowToFile`, `deleteWorkflow` (zusätzlich zu den bestehenden Lese-/Edit-Tools).
+3. **Pilot-Workflow als File** — Konkrete Lieferung: Workflow-JSON nach Schema, Step-5 AI-Prompt, Datenextraktion aus Trustee-DB.
+
+**Risiko bei Nicht-Umsetzung:** PWG-Pilot kann nicht termingerecht (Sommer 2026) aufgesetzt werden. Workflows bleiben nicht portabel zwischen Mandanten/Instanzen, was Demo-Vorbereitungen und Customer-Onboarding deutlich verlangsamt.
+
+**Abhängigkeiten:**
+- Abacus-Testmandant (extern, Patrick koordiniert) — nicht Blocker für Workflow-Bau, nur für End-to-End-Test.
+- Bestehende Komponenten (alle ✅): Trigger, SharePoint-Nodes, `flow.loop`, `trustee.extractFromFiles`, `ai.prompt`, `email.send`.
+
+## Fokus und kritische Details
+
+- **Trustee-DB hat kein Mieter-/Mietzins-Modell** (siehe Codebase-Audit). Stattdessen: Matching-Logik liegt im AI-Prompt von Step 5. Daten werden via Sub-Agent (`queryFeatureInstance` / `aggregateTable` auf `TrusteeDataContact` + `TrusteeDataJournalLine`) oder via dediziertem Daten-Extract-Node bereitgestellt. **Kein neues DB-Schema nötig.**
+- **Graph-Format-Inkonsistenz:** Bootstrap-Templates verwenden Node-Felder `x` / `y` (top-level), Agent-Tool `addNode` schreibt `position: {x, y}`. Beim Import muss normalisiert werden.
+- **Workflow-Envelope-Felder:** `AutoWorkflow` hat zahlreiche optionale Felder (`tags`, `invocations`, `templateScope`, `sharedReadOnly`, `notifyOnFailure`). Beim Export müssen Mandanten-spezifische Felder (`mandateId`, `featureInstanceId`, `id`, `currentVersionId`, `eventId`) **rausgefiltert** werden, sonst sind Files nicht portabel.
+- **Schema-Versionierung:** Wir starten mit `$schemaVersion: "1.0"`. Loader prüft Version und lehnt unbekannte ab oder migriert.
+- **UDB-Erkennung:** Workflow-Files identifizieren wir per Dateiendung `.workflow.json` (case-insensitive) **und** Content-Sniffing (Top-Level-Keys `$schemaVersion` + `graph` + `nodes`).
+- **Sicherheit beim Import:** Geladene Workflows werden NICHT automatisch ausgeführt. Nutzer muss aktiv via "Speichern + Aktivieren" die `invocations` aktivieren. `active` wird beim Import auf `false` gesetzt.
+- **Node-Type-Validierung:** Beim Import gegen `STATIC_NODE_TYPES` validieren. Unbekannte Node-Typen → Fehler mit klarer Meldung (welcher Typ fehlt).
+
+## Ziel und Nicht-Ziele
+
+**Ziel:**
+- Workflows können als File (JSON) aus dem Graph-Editor exportiert und in UDB gespeichert werden.
+- Workflow-Files in UDB können im Graph-Editor wieder geladen werden (neuer Workflow oder Update bestehender).
+- AI-Agent kann Workflows lesen, erstellen, aus File importieren, exportieren und löschen.
+- Der PWG-Pilot-Workflow `pwg-mietzinsbestaetigung-pilot.workflow.json` liegt im Repo, kann von einer leeren PWG-Demo-Instanz geladen und (mit Test-Daten) ausgeführt werden.
+- Step-5-Prompt liefert pro Scan: Status (`bestaetigt` / `abweichung` / `unleserlich` / `keine_unterschrift`) und einen Antwortvorschlag bei Abweichung.
+
+**Explizit NICHT:**
+- Kein neues DB-Schema für Mietzins/Mieter (Matching erfolgt im Prompt gegen bestehende Trustee-Daten).
+- Keine Persistenz der Pilot-Run-Ergebnisse in einer Trustee-Sub-Tabelle (CSV im Mail reicht für Pilot).
+- Keine Auto-Aktivierung importierter Workflows (sicherheitsrelevant).
+- Keine Migration-Logik für `$schemaVersion > 1.0` (kommt später).
+- Kein Versions-Branching/Diff von Workflow-Files (späteres Thema).
+- Keine SharePoint-OCR-Verbesserungen (extractFromFiles wird wie bestehend genutzt).
+
+## Betroffene Module
+
+- **Gateway:**
+ - `features/graphicalEditor/routeFeatureGraphicalEditor.py` — neue Routen `POST .../workflows/import` und `GET .../workflows/{id}/export`.
+ - `features/graphicalEditor/interfaceFeatureGraphicalEditor.py` — neue Methoden `importWorkflowFromDict`, `exportWorkflowToDict`.
+ - `features/graphicalEditor/_workflowFileSchema.py` (NEU) — Schema-Definition + Validierung + Normalisierung (`x/y` ↔ `position`).
+ - `serviceCenter/services/serviceAgent/workflowTools.py` — neue Tools `createWorkflow`, `createWorkflowFromFile`, `exportWorkflowToFile`, `deleteWorkflow`.
+ - `serviceCenter/services/serviceAgent/toolboxRegistry.py` — Tool-Liste in `workflow`-Toolbox erweitern.
+- **Frontend:**
+ - `components/UnifiedDataBar/FilesTab.tsx` — Workflow-File-Erkennung + Action "In Graph-Editor laden".
+ - `pages/views/workflow/` (Graph-Editor-View) — Buttons "Aus Datei importieren" + "Als Datei exportieren" (Editor-Toolbar).
+ - `api/workflowApi.ts` (oder analog) — neue Endpoint-Wrapper.
+- **DB-Migration:** **nein**.
+- **Repo-Asset:** `gateway/demoData/workflows/pwg-mietzinsbestaetigung-pilot.workflow.json` (NEU).
+- **Andere:** Step-5-Prompt-Template als Konstante in `gateway/modules/features/trustee/promptTemplates/_pwgMietzinsCheck.py` oder direkt im Workflow-File (Decision unten).
+
+## Entscheidungen
+
+| Datum | Entscheidung | Begründung |
+|-------|-------------|------------|
+| 2026-04-16 | Workflow-File-Format = Envelope + Schema-Version (1.0) | Portabel, zukunftssicher, ein Round-Trip ohne Verlust von Metadaten |
+| 2026-04-16 | Matching-Logik im AI-Prompt von Step 5 (nicht neue DB-Tabelle) | Trustee-DB hat kein Mietzins-Modell; Pilot kommt mit AI + bestehende Sub-Agent-Queries aus |
+| 2026-04-16 | Result-Output = CSV als E-Mail-Anhang (kein DB-Schema) | Schlankster Pilot-Pfad; Persistenz später bei Bedarf nachrüstbar |
+| 2026-04-16 | Agent-Tools = Full-CRUD inkl. `deleteWorkflow` | Agent soll Workflows komplett verwalten können (User-Wunsch) |
+| 2026-04-16 | File-Endung `.workflow.json` + Content-Sniffing | Klare UDB-Identifikation ohne neuen MIME-Type |
+| 2026-04-16 | Import setzt `active: false`; Aktivierung manuell | Sicherheits-Default — kein versehentliches Auto-Run nach Import |
+| 2026-04-16 | Step-5-Prompt im Workflow-File (Parameter von `ai.prompt`-Node) | Das File ist self-contained und im Repo lesbar; keine Code-Änderung beim Tunen |
+| 2026-04-16 | CSV-Sammlung über bestehende `data.aggregate` (collect) + `data.consolidate` (csvJoin) | Idiomatisch, bereits implementiert; kein neuer Node nötig |
+| 2026-04-16 | Neuer Node `trustee.queryData` ist Pflicht | Kein bestehender Node liest Trustee-DB; `ai.prompt` hat keinen Sub-Agent-Tool-Zugriff (verifiziert in `methodAi/actions/process.py`) — `context`-Parameter muss vorher befüllt werden |
+| 2026-04-16 | E-Mail mit Attachment via Erweiterung von `email.draftEmail` (neuer optionaler `attachments`-Parameter), kein neuer Node | Minimal-invasiv; `email.draftEmail` ist bereits gemappt auf `methodOutlook.composeAndDraftEmailWithContext` und kann erweitert werden. Im Pilot wird ein Draft erstellt (kein Auto-Versand), das passt zur "Sicherheits-Default-keine-Auto-Aktion"-Linie |
+
+---
+
+## Architektur
+
+### Workflow-File Schema 1.0
+
+```json
+{
+ "$schemaVersion": "1.0",
+ "$exportedAt": "2026-04-16T10:00:00Z",
+ "$gatewayVersion": "0.x.y",
+ "$kind": "poweron.workflow",
+ "label": "PWG Pilot: Jahresmietzinsbestätigung",
+ "description": "Verarbeitet gescannte Rückantworten der Mietzinsbestätigungen ...",
+ "tags": ["pwg", "pilot", "mietzins"],
+ "templateScope": "instance",
+ "sharedReadOnly": false,
+ "notifyOnFailure": true,
+ "graph": {
+ "nodes": [
+ { "id": "n1", "type": "trigger.manual", "x": 50, "y": 200, "title": "Manueller Start", "parameters": {} }
+ ],
+ "connections": [
+ { "source": "n1", "target": "n2", "sourceOutput": 0, "targetInput": 0 }
+ ]
+ },
+ "invocations": [
+ { "type": "schedule", "cronExpression": "0 22 * * *" }
+ ]
+}
+```
+
+**Felder, die NIE im File stehen:** `id`, `mandateId`, `featureInstanceId`, `currentVersionId`, `eventId`, `createdAt`, `updatedAt`, `active` (wird beim Import auf `false` gesetzt).
+
+**Normalisierung beim Import:** Falls Node `position: {x, y}` enthält → in `x`/`y` top-level umwandeln (oder umgekehrt — wir entscheiden uns für **`x`/`y` top-level** als kanonische Form, weil Bootstrap-Templates so aussehen).
+
+### API-Endpunkte (neu)
+
+| Methode | Pfad | Zweck |
+|---------|------|-------|
+| `POST /api/workflows/{instanceId}/workflows/import` | Body: `{ "fileId": "...", "mode": "create" \| "updateGraph", "targetWorkflowId"?: "..." }` | Lädt File aus UDB, validiert, erstellt neuen Workflow oder ersetzt graph eines bestehenden |
+| `GET /api/workflows/{instanceId}/workflows/{workflowId}/export` | Query: `?asFileId=true&folderId=...` | Exportiert Workflow als File. Wenn `asFileId=true`: speichert in UDB und gibt `fileId` zurück. Sonst: gibt JSON-Body direkt zurück (Browser-Download) |
+
+### Frontend-Erweiterungen
+
+**UDB FilesTab** (`FilesTab.tsx`):
+- Erkennt Workflow-Files (`.workflow.json` oder Top-Level `$kind === "poweron.workflow"`).
+- Workflow-File-Eintrag erhält Icon (z. B. Workflow/Diagramm) und Context-Menu-Eintrag **"In Graph-Editor laden"**.
+- Bei Klick → Modal: Ziel-Editor-Instanz wählen + Mode (`create` / `updateGraph` für aktiven Workflow) → POST `/import`.
+
+**Graph-Editor Toolbar** (Editor-View):
+- Button **"Importieren"** → File-Picker (UDB-Files mit Filter `.workflow.json`) → `POST /import`.
+- Button **"Exportieren"** → Modal: Ziel = Download oder UDB-Folder → `GET /export?asFileId=...`.
+
+### AI-Agent Tools (Full-CRUD im `workflow`-Toolbox)
+
+Erweiterung von `workflowTools.py` und Toolbox-Registry:
+
+| Tool | Bestehend? | Beschreibung |
+|------|-----------|--------------|
+| `readWorkflowGraph` | ✅ | Graph eines Workflows lesen |
+| `addNode`, `removeNode`, `connectNodes`, `setNodeParameter` | ✅ | Bestehende Edit-Tools |
+| `listAvailableNodeTypes`, `validateGraph` | ✅ | Bestehende Helpers |
+| `listWorkflowHistory`, `readWorkflowMessages` | ✅ | Bestehende Read-Tools |
+| `createWorkflow` | **NEU** | Args: `label`, `description?`, `tags?`, `graph`, `invocations?`. Ergebnis: `{ workflowId }` |
+| `createWorkflowFromFile` | **NEU** | Args: `fileId`, `mode` (default `create`). Ergebnis: `{ workflowId, importedNodes, warnings }` |
+| `exportWorkflowToFile` | **NEU** | Args: `workflowId`, `targetFolderId?`. Ergebnis: `{ fileId, fileName }` |
+| `deleteWorkflow` | **NEU** | Args: `workflowId`, `confirm: true`. Sicherheits-Confirm-Flag |
+
+Implementierung: alle neuen Tools rufen die neuen Routen / Interface-Methoden auf, kein direkter DB-Zugriff aus den Tools.
+
+---
+
+## Pilot-Workflow: Inhalt
+
+### Datei: `gateway/demoData/workflows/pwg-mietzinsbestaetigung-pilot.workflow.json`
+
+**Knoten-Übersicht (10 Nodes — basierend auf real verfügbaren Nodes nach Codebase-Audit):**
+
+| ID | Type | Title | Wichtige Parameter |
+|----|------|-------|---------------------|
+| `n1` | `trigger.manual` | Manueller Start | (alternativ `trigger.schedule` mit Cron `0 22 * * *`) |
+| `n2` | `sharepoint.listFiles` | Scan-Ordner auflisten | `connectionReference`, `sharepointFolder: "PWG/Mietzinsbestaetigungen/Scans-{year}-{quarter}"`, `filter: { extensions: ["pdf", "tif", "jpg"] }` |
+| `n3` | `flow.loop` | Pro Dokument | `inputArrayPath`, `itemAlias: "scanFile"` |
+| `n4` | `sharepoint.downloadFile` | Datei laden | `fileReference: "{{loop.item}}"` |
+| `n5` | `trustee.extractFromFiles` | OCR & Extraktion | `featureInstanceId`, `prompt: ` |
+| `n6` | `trustee.queryData` ⚠️ NEU | Referenzdaten holen | `featureInstanceId`, `mode: "lookup"`, `entity: "tenantWithRent"`, `tenantNameRef: "{{n5.output.tenantName}}"`, `tenantAddressRef: "{{n5.output.tenantAddress}}"`, `period: "{{currentYear}}"` |
+| `n7` | `ai.prompt` | Prüfung & Klassifikation | `aiPrompt: `, `outputFormat: "json"`, `documentList: `, `context: ` |
+| `n8` | `data.aggregate` | Ergebnisse sammeln (im Loop) | `mode: "collect"` — sammelt `n7.output` über Loop-Iterationen |
+| `n9` | `data.consolidate` | CSV bauen (nach Loop) | `mode: "csvJoin"`, `separator: "\n"` — wandelt gesammelte JSON-Items in CSV |
+| `n10` | `email.draftEmail` | Draft mit Anhang | `connectionReference`, `to: "sachbearbeiter@pwg.ch"`, `subject: "Mietzinsbestätigungen Auswertung {{date}}"`, `body: `, `attachments: [{ name: "ergebnisse.csv", contentRef: "{{n9.output}}" }]` ⚠️ `attachments`-Param muss in `email.draftEmail` ergänzt werden |
+
+**Nicht-existierende Nodes/Params, die als Sub-Aufgabe gebaut werden müssen** (siehe Phase 4):
+1. **`trustee.queryData`** — neuer Node in `nodeDefinitions/trustee.py` + Action in `methodTrustee/actions/queryData.py`. Wrappt `FeatureDataProvider.queryTable` / `aggregateTable`. Mode `lookup` mit Entity `tenantWithRent` macht intern: Match in `TrusteeDataContact` (Debitor) → Aggregat über `TrusteeDataJournalLine` für Mietzins-Konten in der Periode.
+2. **`email.draftEmail.attachments`** — optionaler Parameter `attachments` in `nodeDefinitions/email.py` ergänzen + im Executor `composeAndDraftEmailWithContext` Attachment-Handling implementieren.
+
+**Datenfluss-Anmerkungen:**
+- Der `context`-Parameter von `ai.prompt` muss VOR dem Node befüllt werden (verifiziert: `methodAi/actions/process.py` macht direkten `AiCallRequest` ohne Sub-Agent-Tools). Daher liegt `n6` zwingend vor `n7` im Wire-Pfad.
+- `data.aggregate` (mode `collect`) sammelt im Loop-Body, `data.consolidate` (mode `csvJoin`) führt nach dem Loop zusammen — Standardmuster für CSV-Output aus Loop-Iterationen.
+- `helpers/csvProcessing.py` (in `methodAi`) kann von `data.consolidate` und/oder `email.draftEmail`-Attachment-Builder wiederverwendet werden.
+
+### Extraktionsschema für `n5` (`extractionSchema: "mietzinsbestaetigung"`)
+
+Erwartete Felder im OCR-Output:
+
+```json
+{
+ "tenantName": "string",
+ "tenantAddress": "string",
+ "objectAddress": "string",
+ "confirmedRentAmount": "number|null",
+ "currency": "CHF",
+ "period": "string (z.B. 2026)",
+ "tenantNotes": "string|null",
+ "hasSignature": "boolean",
+ "documentDate": "string (ISO date)|null",
+ "ocrConfidence": "number (0-1)"
+}
+```
+
+### Step-5-Prompt (Inhalt für `n7.parameters.prompt`)
+
+```text
+Du bist ein Sachbearbeitungs-Assistent der Stiftung PWG. Deine Aufgabe ist es,
+eine eingescannte und OCR-extrahierte Jahresmietzinsbestätigung gegen die
+Stammdaten der Buchhaltung (Trustee-Feature) abzugleichen.
+
+Eingaben:
+1. SCAN_DATEN (extrahiert per OCR aus dem Rückantwort-Dokument):
+ {{scan}}
+
+2. REFERENZ_DATEN (aus Trustee-DB für diesen Mieter; ggf. leer wenn nicht
+ eindeutig zuordenbar):
+ {{reference}}
+
+Vorgehen:
+1. Prüfe Identität: Stimmt SCAN_DATEN.tenantName + SCAN_DATEN.tenantAddress mit
+ einem Datensatz in REFERENZ_DATEN.contacts überein? (Toleranz: kleine
+ Tippfehler, Umlaute, Abkürzungen)
+2. Prüfe Mietzinsbetrag: Stimmt SCAN_DATEN.confirmedRentAmount mit dem aus
+ REFERENZ_DATEN.journalLines abgeleiteten erwarteten Mietzins überein?
+ (Toleranz: ±1 CHF Rundung)
+3. Prüfe Unterschrift: hasSignature muss true sein.
+4. Prüfe OCR-Qualität: ocrConfidence < 0.6 → "unleserlich".
+
+Klassifiziere in EXAKT EINEN Status:
+- "bestaetigt": Identität stimmt, Betrag stimmt, Unterschrift vorhanden.
+- "abweichung_betrag": Identität ok, Unterschrift ok, Betrag weicht ab.
+- "abweichung_anmerkung": tenantNotes enthält substantielle Anmerkung
+ (nicht leer, nicht reine Bestätigung).
+- "keine_unterschrift": hasSignature == false.
+- "unleserlich": OCR-Qualität ungenügend ODER Pflichtfelder fehlen.
+- "kein_match": Mieter nicht in REFERENZ_DATEN auffindbar.
+
+Bei Status != "bestaetigt": Generiere einen kurzen, höflichen
+Antwortvorschlag (deutsch, Sie-Form, max. 5 Sätze, PWG-Stil) für die
+Sachbearbeitung. Bei "bestaetigt": antwortVorschlag = null.
+
+Antworte AUSSCHLIESSLICH als JSON nach folgendem Schema:
+{
+ "tenantName": string,
+ "objectAddress": string,
+ "status": "bestaetigt" | "abweichung_betrag" | "abweichung_anmerkung"
+ | "keine_unterschrift" | "unleserlich" | "kein_match",
+ "scanRentAmount": number | null,
+ "expectedRentAmount": number | null,
+ "delta": number | null,
+ "tenantNotes": string | null,
+ "antwortVorschlag": string | null,
+ "matchConfidence": number,
+ "auditEvidence": string
+}
+```
+
+`outputSchema` im Node erzwingt JSON-Form (existierende `ai.prompt`-Capability nutzen).
+
+---
+
+## Umsetzungs-Checkliste
+
+### Phase 1 — Workflow-File-IO Backend
+
+- [ ] **Schema-Modul `_workflowFileSchema.py`** mit `WORKFLOW_FILE_SCHEMA_VERSION = "1.0"`, Funktionen `_validateFileEnvelope()`, `_normalizeNodePositions()`, `_stripPersistenceFields()`, `_buildFileFromWorkflow()`.
+- [ ] **Interface-Methoden** in `interfaceFeatureGraphicalEditor.py`:
+ - `importWorkflowFromDict(envelope, mandateId, featureInstanceId, mode, targetWorkflowId?) -> workflowId`
+ - `exportWorkflowToDict(workflowId) -> envelope`
+- [ ] **Routen** in `routeFeatureGraphicalEditor.py`:
+ - `POST /{instanceId}/workflows/import` (Body: `fileId`, `mode`, `targetWorkflowId?`).
+ - `GET /{instanceId}/workflows/{workflowId}/export` (Query: `asFileId`, `folderId?`).
+- [ ] Validierungen:
+ - Bekannte Schema-Version, Pflichtfelder, Node-Typen vorhanden in `STATIC_NODE_TYPES`.
+ - Bei Import: `active=false` erzwingen.
+- [ ] Unit-Tests: Round-Trip (Export → Import → erneuter Export, Bytes identisch nach Normalisierung).
+
+### Phase 2 — UDB-Erkennung & Frontend-IO
+
+- [ ] **`FilesTab.tsx`**:
+ - Workflow-File-Detection (Endung + Content-Sniffing über `useFiles.isJsonContent`).
+ - Workflow-Icon, Context-Menu-Eintrag "In Graph-Editor laden" → öffnet Modal.
+- [ ] **Modal "Workflow importieren"**: Editor-Instanz-Auswahl, Mode-Wahl (Neu / Bestehenden ersetzen), Submit → API.
+- [ ] **Graph-Editor-Toolbar**: Buttons "Importieren" (Files-Picker) und "Exportieren" (Download-vs-UDB-Modal).
+- [ ] **`api/workflowApi.ts`** (oder analog): `importWorkflowFromFile(fileId, …)`, `exportWorkflowToFile(workflowId, …)`.
+
+### Phase 3 — Agent-Tools Full-CRUD
+
+- [ ] In `workflowTools.py` neue Funktionen: `_createWorkflow`, `_createWorkflowFromFile`, `_exportWorkflowToFile`, `_deleteWorkflow` (alle mit `_`-Prefix gemäss Naming-Konvention).
+- [ ] Tool-Definitionen in `getWorkflowToolDefinitions()` ergänzen.
+- [ ] `toolboxRegistry.py` — Tool-Liste in `workflow`-Toolbox erweitern.
+- [ ] Sicherheits-Confirm: `deleteWorkflow` verlangt `confirm: true`.
+- [ ] Integration-Test: Agent erstellt Workflow → exportiert → löscht → re-importiert via fileId.
+
+### Phase 4 — Fehlende Nodes für Pilot ergänzen (konkretisiert nach Codebase-Audit)
+
+**Audit-Ergebnis** (siehe auch Tabelle in Abschnitt "Findings" weiter oben):
+- ✅ `data.aggregate` (mode `collect`) und `data.consolidate` (mode `csvJoin`) existieren bereits → CSV-Sammlung ist abgedeckt, **kein neuer Daten-Node nötig**.
+- ❌ Kein Node liest Trustee-DB-Daten → `trustee.queryData` muss neu gebaut werden.
+- ❌ `email.draftEmail` hat keinen `attachments`-Parameter → muss erweitert werden.
+- ✅ `ai.prompt` mit `outputFormat: "json"` und `context`-Parameter ist passgenau für Step 5.
+
+**Sub-Task 4a — Neuer Node `trustee.queryData`:**
+- [ ] Definition in `gateway/modules/features/graphicalEditor/nodeDefinitions/trustee.py` ergänzen (Pattern wie `trustee.refreshAccountingData`).
+ - Parameter: `featureInstanceId` (hidden), `mode` (select: `lookup`, `aggregate`, `raw`), `entity` (select: `tenantWithRent`, `contact`, `journalLines`, `accounts`), `tenantNameRef`/`tenantAddressRef`/`period` (text, optional je nach mode), `extraFilter` (json, optional).
+ - Inputs: 1, Outputs: 1, `outputPorts: { 0: { schema: "QueryResult" } }`.
+ - `_method: "trustee"`, `_action: "queryData"`.
+- [ ] Action-Implementierung `gateway/modules/workflows/methods/methodTrustee/actions/queryData.py`:
+ - Mode `lookup` mit Entity `tenantWithRent` → fuzzy match in `TrusteeDataContact` (Debitor-Filter), dann Aggregat in `TrusteeDataJournalLine` über die für Mietzins relevanten Konten in der Periode.
+ - Mode `raw` → direkter `FeatureDataProvider.queryTable` mit `entity` als Tabellenname.
+ - Mode `aggregate` → `FeatureDataProvider.aggregateTable` mit `extraFilter`.
+ - Output-Format: `{ matched: bool, contacts: [...], journalLines: [...], expectedRentAmount: number|null, matchConfidence: number }` — direkt verwendbar als `context` in `ai.prompt`.
+- [ ] Registrierung in `methodTrustee/__init__.py` und `methodTrustee.py` (analog zu existierenden Actions).
+- [ ] Unit-Test `tests/methods/methodTrustee/test_queryData.py`: Mock `FeatureDataProvider`, prüfe Match-Logik mit Tippfehler-Toleranz und Mietzins-Aggregation.
+
+**Sub-Task 4b — Attachment-Support in `email.draftEmail`:**
+- [ ] In `nodeDefinitions/email.py` neuen optionalen Parameter ergänzen:
+ ```python
+ {"name": "attachments", "type": "json", "required": False, "frontendType": "attachmentBuilder",
+ "description": t("Anhänge (Liste von { name, contentRef | csvFromVariable | base64Content })"),
+ "default": []},
+ ```
+- [ ] In `gateway/modules/workflows/methods/methodOutlook/actions/composeAndDraftEmailWithContext.py` (oder dem aktuellen Pfad) Attachment-Handling implementieren:
+ - `contentRef` → resolved aus Wire/Variable, als Bytes/String.
+ - `csvFromVariable` → resolved aus Variable (z. B. Output von `data.consolidate`) und als CSV-Anhang gehängt.
+ - `base64Content` → direkt dekodieren.
+ - Hochladen als Outlook-Draft-Attachment via Graph-API (`/me/messages/{id}/attachments`).
+- [ ] Frontend: Attachment-Builder im Property-Panel des Editors (kann minimal sein — JSON-Editor reicht für Pilot).
+- [ ] Unit-Test mit gemocktem Outlook-Connector: prüfe dass Attachment korrekt im erstellten Draft enthalten ist.
+
+**Sub-Task 4c — Frontend-Anpassungen für neue Nodes:**
+- [ ] `trustee.queryData` automatisch in der Node-Palette sichtbar (kommt durch Definition gratis).
+- [ ] Property-Panel: Bei `mode: "lookup"` und `entity: "tenantWithRent"` die Tenant-Felder anzeigen, sonst ausblenden (`dependsOn`-Mechanismus wie in `sharepointFolder`).
+- [ ] Falls beide Wege zu aufwändig: Pilot-Workflow auf 7 Nodes vereinfachen — Step 5 macht ALLES (Sub-Agent-Query + Klassifikation + Antwort) in einem `ai.prompt`-Node mit `featureSubAgent`-Tool-Zugriff.
+
+### Phase 5 — Pilot-Workflow als File
+
+- [ ] Datei `gateway/demoData/workflows/pwg-mietzinsbestaetigung-pilot.workflow.json` erstellen (Inhalt wie oben spezifiziert).
+- [ ] Datei via Test in eine PWG-Demo-Instanz importieren (manueller Smoke-Test).
+- [ ] Test-Lauf mit 2–3 fiktiven Scan-PDFs (in `gateway/demoData/pwg/scans/` ablegen — Sub-Aufgabe).
+- [ ] CSV-Ergebnis prüfen: enthält pro Scan eine Zeile mit allen Feldern aus dem Output-Schema.
+
+### Querschnitt
+
+- [ ] **API-Endpunkte:** ja, 2 neue (Import + Export).
+- [ ] **DB-Schema / Migration:** **nein**.
+- [ ] **Frontend-Komponenten:** FilesTab-Erweiterung, Graph-Editor-Toolbar-Buttons, Import-Modal.
+- [ ] **RBAC / Permissions:** Import/Export benötigen gleiche Rechte wie `update_workflow`. `deleteWorkflow`-Tool nur wenn User-Rolle `delete_workflow` darf.
+- [ ] **Neutralisierung betroffen?** Indirekt ja: Workflow-Files können sensible Parameter (Connection-Refs, E-Mail-Adressen) enthalten. **Doku-Hinweis** beim Export-Modal: "Vor Weitergabe Datei prüfen". Keine automatische Neutralisierung im Pilot-Scope.
+- [ ] **Navigation / Routing:** keine Änderung.
+- [ ] **Billing-Impact:** Pilot-Workflow verbraucht Tokens (1 AI-Call pro Scan, ggf. Sub-Agent-Calls). Kalkulation für PWG-Pilot: `~3'200 Schreiben/Jahr × ~3 Calls × ~2k Tokens` als Grössenordnung — gehört in PWG-Preismodell-Action-Item AI3.
+
+---
+
+## Akzeptanzkriterien
+
+| # | Kriterium (Given-When-Then) | Prio |
+|---|---------------------------|------|
+| 1 | Given Workflow im Graph-Editor, When User auf "Exportieren → UDB" klickt, Then liegt eine `