fixes udb

This commit is contained in:
ValueOn AG 2026-05-27 16:49:03 +02:00
parent 51e6ef076d
commit 8ee1327522
9 changed files with 556 additions and 40 deletions

View file

@ -1,5 +1,5 @@
<!-- status: canonical -->
<!-- lastReviewed: 2026-05-12 -->
<!-- lastReviewed: 2026-05-23 -->
# Themen-Index für AI-Kontext
@ -35,7 +35,8 @@ Lade immer zuerst diese Datei. Dann gezielt die passende(n) Referenz-Datei(en).
| Thema | Datei | Wann laden |
|-------|-------|------------|
| Neutralisierung | b-reference/platform/neutralization.md | Datenschutz, AI-Call-Pipeline, Failsafe |
| RBAC | b-reference/platform/rbac.md | 4-Stufen-Modell, Template-Rollen, Resolution, Datenmodell |
| Unified Data Bar (UDB) | b-reference/platform/unified-data-bar.md | Polymorphe `UdbNode`-Hierarchie, Flag-Mechanik (`neutralize`/`scope`/`ragIndexEnabled`), `POST /api/udb/tree/children`, `POST /api/udb/node/{key}/flag/{flag}`, RBAC fuer Flag-Edits (Owner vs Feature-Admin) |
| RBAC | b-reference/platform/rbac.md | 4-Stufen-Modell, Template-Rollen, Resolution, Datenmodell, UDB-Flag-Edit-Gate (Feature-Admin) |
| 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 |
@ -52,6 +53,7 @@ Lade immer zuerst diese Datei. Dann gezielt die passende(n) Referenz-Datei(en).
| UDB DataSource Settings (RAG-Limits) | c-work/2-build/2026-05-udb-datasource-settings.md | Settings-Icon ⚙️ pro Tree-Node, `DataSource.settings.ragLimits` als alleinige Quelle (kein Override-Layer), PATCH `/api/datasources/{id}/settings`, GET `/api/datasources/{id}/cost-estimate`, indikative CHF-Schätzung |
| UDB Cascade-Inherit für DataSource-Flags | c-work/4-done/2026-05-udb-cascade-inherit.md | 3-wertige `neutralize`/`ragIndexEnabled`/`scope` (`null` = vererbt), `_inheritFlags.getEffectiveFlag()` (Path-Traversal), `cascadeResetDescendants()` setzt explizite Descendants-Werte beim Parent-Toggle auf `null`, Walker konsumieren pre-resolved Werte aus `_loadRagEnabledDataSources` |
| UDB Generic Tree Refactor (BE autoritativ, FE pure Renderer) | c-work/4-done/2026-05-udb-generic-tree-refactor.md | Single `POST /api/workspace/{id}/tree/children` mit `{parents}`-Liste liefert `nodesByParent` inkl. pre-computed `effectiveNeutralize/Scope/RagIndexEnabled` als `boolean\|'mixed'`. Orchestrator `serviceKnowledge/_buildTree.py`. SourcesTab.tsx auf ~530 Zeilen, kein optimistic update, Spinner pro pending Toggle, einheitliches Mixed-Symbol. FDS unterstuetzt nun RAG (gleiche 3-wertige Semantik wie DS). Geloescht: 7 alte Tree-Endpoints (`/resolve-flags`, `/connections`, `/connections/{id}/services`, `/connections/{id}/browse`, `/feature-connections`, `/feature-connections/{fiId}/tables`, `/feature-connections/{fiId}/parent-objects/{tableName}`). Bewusst weggelassen: FDS-Record-Expansion (Tabellen-Ebene reicht). |
| UDB Polymorphic Refactor (Object-orientiert + Generic Router) | c-work/3-validate/2026-05-udb-polymorphic-refactor.md | Neue Klassenhierarchie `serviceKnowledge/udbNodes.py` (`UdbNode` ABC, Subklassen pro Kind). Generischer Router `routes/routeUdb.py` mit `POST /api/udb/tree/children` und `POST /api/udb/node/{key}/flag/{flag}`. Hart-Cut der alten 4 PATCH-Routen (`/scope`, `/neutralize`, `/rag-index`, `/neutralize-fields`) und der `/api/workspace/{id}/tree/children`-Route. FDS-Schema: `userId`, `workspaceInstanceId`, `scope` entfernt — FDS ist feature-owned, RBAC-gated (`-admin` Rolle auf `featureInstanceId`). Kanonische Doku: `b-reference/platform/unified-data-bar.md`. |
| Zentrale Workflow-Admin (Meine Sicht) | c-work/1-plan/2026-04-automation-central-admin.md | `/automations` Tabs Dashboard + Workflows, `GET .../workflow-runs/workflows` |
| Web Image Search | c-work/1-plan/2026-03-web-image-search.md | WEB_SEARCH_MEDIA Feature |
| UI i18n / Sprachsets (done) | c-work/3-validate/2026-04-ui-i18n-dynamic-language-sets.md | Mehrsprachigkeit, `t()`, Sprachset-API, Admin-UI, AI-Übersetzung |

View file

@ -1,6 +1,6 @@
<!-- status: canonical -->
<!-- lastReviewed: 2026-05-18 -->
<!-- verifiedAgainst: gateway (codebase audit 2026-04-16, RAG Consent & Control update 2026-05-15, Cascade-Inherit + Generic Tree Refactor update 2026-05-18) -->
<!-- lastReviewed: 2026-05-23 -->
<!-- verifiedAgainst: gateway (codebase audit 2026-04-16, RAG Consent & Control update 2026-05-15, Cascade-Inherit + Generic Tree Refactor update 2026-05-18, Polymorphic UDB Refactor 2026-05-23) -->
# Neutralisierungs-System
@ -47,7 +47,7 @@ Im zentralen AI-Gate (`_neutralizeRequest`):
1. **Toggle `FileItem.neutralize`:** Index und Chunks werden synchron gelöscht; Re-Index im Hintergrund verhindert Leaks bei Fehlern in der Nachindexierung.
2. **Toggle `FileFolder.neutralize`:** Propagiert `neutralize` rekursiv auf alle Dateien im Ordner; Index/Chunks werden pro Datei synchron gelöscht, Re-Index im Hintergrund. Neue/verschobene Dateien erben das Flag des Ziel-Ordners.
3. **Indexierung:** `indexFile()` neutralisiert Text-Chunks bei `FileItem.neutralize=True` → Data-at-rest abgesichert.
4. **DataSource-Download / RAG-Indexierung:** `DataSource.neutralize` ist die einzige Quelle für die Neutralisierungs-Policy pro Tree-Element. **Seit 2026-05 (Cascade-Inherit):** `neutralize`, `ragIndexEnabled` und `scope` sind 3-wertig (`null` = vererbt, `True/False` bzw. ein Scope-String = explizit). `null` in der Datenbank bedeutet: Wert vom nächsten Vorfahren-DataSource im Path-Tree übernehmen (longest-prefix wins, gleicher `connectionId` + `sourceType`). Beim Setzen eines expliziten Werts auf einem Parent **kaskadiert das Backend** (`cascadeResetDescendants`) durch alle Descendants und setzt deren explizite Werte für **dieses eine Flag** auf `null` zurück — Descendants die bereits geerbt haben, werden nicht angefasst. Walker konsumieren pre-resolved Werte: `_loadRagEnabledDataSources` (in `subConnectorIngestConsumer.py`) berechnet pro DS via `_inheritFlags.getEffectiveFlag()` den effektiven `ragIndexEnabled`/`neutralize`/`scope` und schreibt das in das Walker-Input-Dict — die Sync-Walker (`subConnectorSync*.py`) bleiben dadurch unverändert und lesen weiter direkt `ds.get("neutralize", False)`. Die Flag-Werte werden zusätzlich auf erzeugte `FileItem`s übernommen. Der frühere `subPolicyResolver` ist auf einen Backward-Compat-Shim auf `getEffectiveFlag` reduziert. **Seit 2026-05-18 (Generic Tree Refactor):** `FeatureDataSource` traegt nun ebenfalls `ragIndexEnabled` mit gleicher 3-wertiger Semantik (`_INHERITABLE_FDS_FLAGS` enthaelt alle drei Flags); `PATCH /api/datasources/{id}/rag-index` akzeptiert via `_findSourceRecord` sowohl DS als auch FDS — Bootstrap-Job und Chunk-Purge bleiben aber DS-only (FDS-RAG ist Feature-Pipeline-getrieben, das Flag steuert ausschliesslich die Pipeline-Aktivierung).
4. **DataSource-Download / RAG-Indexierung:** `DataSource.neutralize` ist die einzige Quelle für die Neutralisierungs-Policy pro Tree-Element. **Seit 2026-05 (Cascade-Inherit):** `neutralize`, `ragIndexEnabled` und `scope` sind 3-wertig (`null` = vererbt, `True/False` bzw. ein Scope-String = explizit). `null` in der Datenbank bedeutet: Wert vom nächsten Vorfahren-DataSource im Path-Tree übernehmen (longest-prefix wins, gleicher `connectionId` + `sourceType`). Beim Setzen eines expliziten Werts auf einem Parent **kaskadiert das Backend** (`cascadeResetDescendants`) durch alle Descendants und setzt deren explizite Werte für **dieses eine Flag** auf `null` zurück — Descendants die bereits geerbt haben, werden nicht angefasst. Walker konsumieren pre-resolved Werte: `_loadRagEnabledDataSources` (in `subConnectorIngestConsumer.py`) berechnet pro DS via `_inheritFlags.getEffectiveFlag()` den effektiven `ragIndexEnabled`/`neutralize`/`scope` und schreibt das in das Walker-Input-Dict — die Sync-Walker (`subConnectorSync*.py`) bleiben dadurch unverändert und lesen weiter direkt `ds.get("neutralize", False)`. Die Flag-Werte werden zusätzlich auf erzeugte `FileItem`s übernommen. Der frühere `subPolicyResolver` ist auf einen Backward-Compat-Shim auf `getEffectiveFlag` reduziert. **Seit 2026-05-23 (Polymorphic UDB Refactor):** `FeatureDataSource` traegt `neutralize`, `ragIndexEnabled` und `neutralizeFields` (kein `scope`, kein `userId`, kein `workspaceInstanceId` mehr — FDS ist feature-owned, RBAC-gated). Vererbung und Cascade-Reset laufen ueber `_INHERITABLE_FDS_FLAGS = {"neutralize", "ragIndexEnabled"}` mit Coordinate `(featureInstanceId, tableName)`. Flag-Toggles laufen ausschliesslich ueber `POST /api/udb/node/{key}/flag/{flag}` (siehe `b-reference/platform/unified-data-bar.md`); die alten `PATCH /api/datasources/{id}/...`-Routen sind entfallen.
5. **FeatureDataSource (Tabellen-Level):** `_queryFeatureInstance()` setzt bei `neutralize=True` u. a. `requireNeutralization=True` für Sub-Agent-AI-Calls.
6. **FeatureDataSource (Feld-Level):** `neutralizeFields` definiert Spalten, deren Werte in `FeatureDataProvider` mit deterministischen Platzhaltern `[NEUT.<field>.<hash>]` ersetzt werden, bevor Daten den Sub-Agent erreichen. Gleichheits-Vergleiche bleiben möglich (stabiler Hash).
7. **AI-Call:** Bei erzwungener Neutralisierung (`requireNeutralization=True`) schlägt fehlgeschlagene Neutralisierung hart fehl (`RuntimeError` / Blockierung bzw. Entfernen betroffener Message-Teile — kein Durchleiten im Rohzustand).
@ -57,12 +57,14 @@ Im zentralen AI-Gate (`_neutralizeRequest`):
## Unified Data Bar — Tree-Endpoint und Flag-Toggling
Seit 2026-05-18 (Generic Tree Refactor, siehe `c-work/4-done/2026-05-udb-generic-tree-refactor.md`) liefert ein einziger Endpoint die komplette sichtbare Tree-Struktur des UDB inklusive aller drei effektiver Flag-Werte:
Vollstaendige Doku: `b-reference/platform/unified-data-bar.md`. Hier nur Bezug zur Neutralisierung:
- **Endpoint:** `POST /api/workspace/{instanceId}/tree/children` mit Body `{parents: [null, "<key1>", "<key2>", ...]}`.
- **Response:** `{nodesByParent: {"__root__": [...], "<key1>": [...], ...}}` — jeder `TreeNode` enthält `effectiveNeutralize`, `effectiveScope`, `effectiveRagIndexEnabled` als `boolean | "mixed"` bzw. `string | "mixed"` (mode=`aggregate`).
- **Builder:** `gateway/modules/serviceCenter/services/serviceKnowledge/_buildTree.py` orchestriert alle Kinds (`connection`, `service`, `folder`, `file`, `mandateGroup`, `featureNode`, `fdsTable`) und ruft `resolveEffectiveForPath` / `resolveEffectiveForFds` pro Node — auch fuer Coordinates ohne eigenen DB-Record (virtueller Datensatz mit `null`-Flags).
- **Frontend:** `SourcesTab.tsx` rendert ausschliesslich die Backend-Antwort, keine Vererbungslogik. Toggle = PATCH -> Refetch -> Render mit Spinner ueber dem Flag-Button waehrend des Round-Trips. Mixed-Symbol `U+25E9` einheitlich fuer alle drei Flags.
- **Tree-Endpoint:** `POST /api/udb/tree/children` mit Body `{parents: [null, "<key1>", ...]}`. Response: `{nodesByParent}` mit pre-computed `effectiveNeutralize | "mixed"` pro Node.
- **Flag-Toggle:** `POST /api/udb/node/{key}/flag/neutralize` mit Body `{value: true|false|null}`. Polymorph: dieselbe Route bedient DataSource, FeatureDataSource und das virtuelle FdsField (Persistenz dort in `FeatureDataSource.neutralizeFields`).
- **Builder:** `platform-core/modules/serviceCenter/services/serviceKnowledge/_buildTree.py` orchestriert alle Kinds; effektive Werte und mixed-Aggregation kommen polymorph aus den `UdbNode`-Subklassen (`udbNodes.py`).
- **RBAC:** DataSource → Owner-of-record; FDS und FdsField → Feature-Admin (`Role.roleLabel.endswith('-admin')` auf der Feature-Instanz). SysAdmin/PlatformAdmin haben **keinen** automatischen Edit-Bypass auf UDB-Flags.
- **Frontend:** `SourcesTab.tsx` rendert ausschliesslich die Backend-Antwort, keine Vererbungslogik. Toggle = POST → Refetch → Render mit Spinner ueber dem Flag-Button waehrend des Round-Trips. Mixed-Symbol `U+25E9` einheitlich fuer alle drei Flags.
- **Klick-Semantik bei mixed:** Klick auf das mixed-Symbol setzt explizit `false` (gilt fuer alle drei Flags). Begruendung: der visuelle Toggle behaelt zwei klare End-Zustaende (`true` / `false`); Reset auf `null`/inherit erfolgt ausschliesslich durch Parent-Toggle (siehe cascade-inherit). Das ist die einzige Geschaeftslogik im Frontend — alles andere kommt vom Backend.
## NeutralizationPanel (Frontend)
@ -82,9 +84,11 @@ Seit 2026-05-18 (Generic Tree Refactor, siehe `c-work/4-done/2026-05-udb-generic
| **Ordner-Toggle / Index** | `routes/routeDataFiles.py``PATCH /folders/{folderId}/neutralize` (rekursive Propagierung) |
| **Workflow-Aktion** | `workflows/methods/methodContext/actions/neutralizeData.py` |
| **Kontext** | `serviceCenter/context.py``requireNeutralization` |
| **Flags (Modelle)** | `datamodels/datamodelFiles.py` (`FileItem.neutralize`), `datamodelFileFolder.py` (`FileFolder.neutralize`), `datamodelDataSource.py`, `datamodelFeatureDataSource.py` (`neutralize`, `neutralizeFields`, `ragIndexEnabled` seit 2026-05-18) |
| **Flags (Modelle)** | `datamodels/datamodelFiles.py` (`FileItem.neutralize`), `datamodelFileFolder.py` (`FileFolder.neutralize`), `datamodelDataSource.py`, `datamodelFeatureDataSource.py` (`neutralize`, `neutralizeFields`, `ragIndexEnabled` — kein `scope`/`userId`/`workspaceInstanceId` seit 2026-05-23) |
| **Effective-Flag-Resolution** | `serviceCenter/services/serviceKnowledge/_inheritFlags.py` (`getEffectiveFlag`, `getEffectiveFlagFds`, `resolveEffectiveForPath`, `resolveEffectiveForFds`, `cascadeResetDescendants`) |
| **UDB Tree-Builder** | `serviceCenter/services/serviceKnowledge/_buildTree.py` (`getChildrenForParents` -> `POST /api/workspace/{id}/tree/children`) |
| **UDB polymorphe Node-Klassen** | `serviceCenter/services/serviceKnowledge/udbNodes.py` (`UdbNode`-Hierarchie, `canEdit` / `getEffectiveFlag` / `setFlag` / `getLogicalChildren`) |
| **UDB Tree-Builder** | `serviceCenter/services/serviceKnowledge/_buildTree.py` (`getChildrenForParents` -> `POST /api/udb/tree/children`) |
| **UDB Generic Router** | `routes/routeUdb.py` (`POST /api/udb/tree/children`, `POST /api/udb/node/{key}/flag/{flag}`) |
| **AI-Operationen / Enums** | `datamodels/datamodelAi.py`, `aicore/aicorePluginPrivateLlm.py` |
## Regeln / Invarianten

View file

@ -1,6 +1,6 @@
<!-- status: canonical -->
<!-- lastReviewed: 2026-04-17 -->
<!-- verifiedAgainst: gateway (codebase audit 2026-04-17) -->
<!-- lastReviewed: 2026-05-23 -->
<!-- verifiedAgainst: gateway (codebase audit 2026-04-17, UDB-FlagEdit-Doku 2026-05-23) -->
# RBAC-System
@ -350,6 +350,20 @@ Felder `id` und Namen mit fuehrendem `_` (z.B. `_createdBy`, `_createdAt`) sind
| RBAC-Rule-Resolution | `gateway/modules/security/rbac.py` |
| TEMPLATE_ROLES (Beispiel) | `gateway/modules/features/workspace/mainWorkspace.py` |
## Feature-Admin-Gate fuer UDB-Flag-Edits
Die Unified Data Bar (siehe `b-reference/platform/unified-data-bar.md`) erlaubt das Editieren von `neutralize` / `ragIndexEnabled` / `neutralizeFields` auf `FeatureDataSource`-Nodes ausschliesslich fuer **Feature-Admins** der jeweiligen Feature-Instanz. Implementierung in `serviceKnowledge/udbNodes.py::_isFeatureAdmin(rootIf, userId, featureInstanceId)`:
1. lade die `FeatureAccess`-Row fuer (userId, featureInstanceId)
2. lade ihre `FeatureAccessRole`-Eintraege und die referenzierten `Role`-Records
3. erlaube nur, wenn mindestens eine Rolle `Role.roleLabel.endswith("-admin")` erfuellt (z.B. `workspace-admin`, `trustee-admin`, ...)
**Wichtig:**
- Es ist kein impliziter Bypass fuer `isSysAdmin` oder `isPlatformAdmin` vorgesehen — UDB-Flag-Edits sind ein **Daten-Verantwortungs-Akt** und nicht ein Plattform-Operations-Akt. Plattform-Administratoren ohne Feature-Admin-Rolle in der Instanz bekommen 403.
- Die DataSource-Familie (`conn|...`, `svc|...`, `ds|...`) wird ueber Ownership (`rec.userId == context.user.id`) gechecked, nicht ueber RBAC-Rollen.
Validierung des Wertes erfolgt in `routeUdb._validateFlagValue`; insbesondere setzt `scope="global"` zusaetzlich `context.isSysAdmin` voraus.
## Regeln / Invarianten
- Mandanten-Rollen und Feature-Instanz-Rollen **strikt trennen** (keine Kreuz-Zuweisung der Label-Typen)

View file

@ -0,0 +1,243 @@
<!-- status: canonical -->
<!-- lastReviewed: 2026-05-27 -->
<!-- verifiedAgainst: platform-core/modules/serviceCenter/services/serviceKnowledge/udbNodes.py | _buildTree.py | _inheritFlags.py | platform-core/modules/routes/routeUdb.py | platform-core/modules/datamodels/datamodelFeatureDataSource.py | ui-nyla/src/components/UnifiedDataBar/UdbSourcesProvider.tsx -->
# Unified Data Bar (UDB)
Die UDB ist die zentrale Komponente zur Anzeige und Steuerung **aller** Datenquellen, die ein User im System sieht. Sie ist **feature-unabhaengig**: sie kann in jeder Feature-Instanz eingebunden werden, ist aber von keinem konkreten Feature abhaengig. Auch ein System-Tab ohne Feature-Kontext koennte die UDB nutzen.
Dieses Dokument ist die kanonische Quelle der Wahrheit fuer:
1. das Domain-Modell (welche Node-Typen, wer besitzt was)
2. die Flag-Mechanik (`neutralize` / `scope` / `ragIndexEnabled`, Vererbung, mixed-Aggregation, Cascade-Reset)
3. die RBAC-Regeln fuer Flag-Aenderungen
4. das API-Schema und den Frontend-Vertrag
> Zugehoerige Bereiche:
> - Neutralisierung (Daten-Gate, Engine, Failsafe): `b-reference/platform/neutralization.md`
> - RAG-Indexierung (Ingestion-Pipeline, Inventory): `b-reference/gateway/ai-agent.md`
> - RBAC (Roles, Permissions, Resolution): `b-reference/platform/rbac.md`
## Architektur-Prinzipien
- **Backend ist autoritativ.** Es liefert pro sichtbarem Tree-Node die effektiven Flag-Werte (`boolean | string | "mixed"`). Das Frontend rendert nur — keine Vererbung, keine Aggregation, keine optimistic Updates.
- **Polymorphes Node-Modell.** Jeder Node-Typ (`UdbNode`-Subklasse) kapselt seine eigene Logik: welche Flags er traegt, wer ihn bearbeiten darf, wie ein Flag persistiert wird, wie effektive Werte berechnet werden. Es gibt keine zentralen `if kind == "fds"`-Stellen mehr.
- **Eine generische API.** Genau ein Endpoint fuer Tree-Walks, genau ein Endpoint fuer Flag-Toggles. Keine pro-Flag-PATCH-Routen.
- **Hart-Cut bei Refactors.** Es gibt keinen Compatibility-Layer auf den alten Endpoints; alte Aufrufer werden umgestellt oder geloescht.
## Domain-Modell
```mermaid
flowchart TD
subgraph synth [Synthetische Container]
personalRoot[personalRoot]
mgrp["mgrp|mandateId"]
end
subgraph dsFamily [DataSource-Familie - user-private]
conn["conn|connId"]
svc["svc|connId|service"]
folder["ds|connId|sourceType|/path/"]
file["ds|connId|sourceType|/path/file.ext"]
end
subgraph fdsFamily [FeatureDataSource-Familie - feature-owned]
feat["feat|mandateId|featureCode|fiId"]
fdstbl["fdstbl|fiId|tableName"]
fdsfld["fdsfld|fiId|tableName|fieldName"]
end
personalRoot --> conn
conn --> svc
svc --> folder
folder --> folder
folder --> file
mgrp --> feat
feat --> fdstbl
fdstbl --> fdsfld
```
### Vier Node-Familien
| Familie | Subklassen | DB-Record | scope | neutralize | ragIndexEnabled | RBAC zum Editieren |
|---|---|---|---|---|---|---|
| **SyntheticContainer** | `SyntheticContainerNode`, `MandateGroupNode` | nein | | | | nie editierbar |
| **DataSource** | `ConnectionNode`, `ServiceNode`, `FolderNode`, `FileNode` | `DataSource` (`userId`-scoped, optional virtuell) | ja (3-wertig) | ja (3-wertig) | ja (3-wertig) | Owner-of-record (`rec.userId == user`) |
| **FdsRecord** | `FdsWorkspaceNode`, `FdsTableNode`, `FdsRowNode` | `FeatureDataSource` (`featureInstanceId`-scoped) | nein | ja (3-wertig) | ja (3-wertig) | Feature-Admin (`roleLabel.endswith('-admin')` auf der `featureInstanceId`) |
| **FdsField** | `FdsFieldNode` | virtueller Child unter Table, persistiert in `FeatureDataSource.neutralizeFields` | nein | ja (zweiwertig: enthalten in `neutralizeFields` oder nicht) | nein | wie FdsRecord |
**Wichtig — Ownership-Trennung:**
- `DataSource` ist **user-privat** (`userId`-scoped). Scope (`personal` | `mandate` | `global`) bestimmt, wer sie zusaetzlich sehen darf. Geteilte Sources erscheinen anderen Usern read-only.
- `FeatureDataSource` (FDS) ist **feature-owned** (`featureInstanceId`-scoped). Es gibt keinen `userId`, kein `workspaceInstanceId`, kein `scope`-Feld. Sichtbarkeit ergibt sich aus RBAC auf der Feature-Instanz. Editierbarkeit verlangt Feature-Admin.
## Flag-Mechanik
Drei Flags mit identischer 3-wertiger Semantik (Ausnahme: FdsField, siehe unten):
- `null` — vererbt vom naechsten expliziten Vorfahren entlang des Path-Trees (longest-prefix wins, gleiche `connectionId` + `sourceType` fuer DS, gleiche `featureInstanceId` + `tableName` fuer FDS).
- `True` / `False` — expliziter Override.
- `"mixed"`**berechneter Anzeige-Wert** auf Parent-Nodes, wenn die direkten Children unterschiedliche effektive Werte haben. Wird vom Backend pro Render geliefert (`mode="aggregate"`) und nie persistiert.
### Vererbung (Path-Traversal)
Implementiert in `_inheritFlags.py`:
- `getEffectiveFlag(...)` / `getEffectiveFlagFds(...)` — laufen das Path-Tree hoch und liefern den ersten expliziten Wert (`True`/`False`/scope-string) oder den Default.
- `resolveEffectiveForPath(...)` / `resolveEffectiveForFds(...)` — geben das vollstaendige Triplett zurueck plus einen virtuellen Datensatz fuer Pfade ohne eigenen DB-Record.
### Aggregation (mixed)
Polymorph in jeder `UdbNode`-Subklasse:
```mermaid
flowchart TD
Render["Render eines Parent-Nodes"] --> CallEff["node.getEffectiveFlag(flag, allDs, allFds, mode='aggregate')"]
CallEff --> GetChildren["node.getLogicalChildren(allDs, allFds)"]
GetChildren --> CompareChildren["Effective-Werte aller Children gleich?"]
CompareChildren -- ja --> ReturnValue["Wert"]
CompareChildren -- nein --> ReturnMixed["'mixed'"]
```
- **`mode="own"`** liefert nur den eigenen effektiven Wert (Path-Walk auf dem eigenen Record / der eigenen Coordinate).
- **`mode="aggregate"`** kombiniert eigenen Wert mit den aggregierten Effective-Werten der `getLogicalChildren(...)`. Sind die Children konsistent, gewinnt deren Wert; uneinheitlich → `"mixed"`.
### Cascade-Reset auf Toggle
Wird ein expliziter Wert auf einem Parent gesetzt, raeumt der Backend (`cascadeResetDescendants` / `cascadeResetDescendantsFds`) **fuer genau dieses Flag** alle expliziten Werte auf Descendant-Records weg (setzt sie auf `null`). Descendants, die bereits geerbt haben, werden nicht angefasst. Der Parent-Wert ist anschliessend die effektive Quelle fuer alle.
Die generische Cascade-Infrastruktur (`_inheritFlags.py`) kennt **nur** die drei Flag-Spalten. Alles darueber hinaus — wie das Aufraumen von `neutralizeFields` — ist Verantwortung der jeweiligen Node-Klasse via dem `_onSetFlag`-Hook in `_FdsFamilyNode`:
- `FdsTableNode._onSetFlag`: wipe-t die eigene `neutralizeFields`-Liste wenn ein expliziter `neutralize`-Wert gesetzt wird (per-column Overrides werden durch den Tabellen-Wert obsolet).
- `FdsWorkspaceNode._onSetFlag`: wipe-t `neutralizeFields` auf allen Descendant-Tables, damit die Cascade-Invariante auch fuer field-level State gilt.
Damit bleibt die Cascade-Infrastruktur flag-agnostisch, und jede Node-Klasse kapselt ihre eigene Speziallogik (Architekturprinzip: **Polymorphes Node-Modell**).
### FdsField-Sonderfall
Felder unter einer FDS-Tabelle (`fdsfld|...`) haben **kein** eigenes Vererbungstriplett. Der Wert wird in der Spalte `FeatureDataSource.neutralizeFields` (Liste von Spalten-Namen) gespeichert und folgt einem Zwei-Quellen-Modell:
1. `fieldName in tableRec.neutralizeFields` → expliziter Override → `effectiveNeutralize=True`.
2. sonst → das Feld **erbt** vom effektiven `neutralize` seiner Tabelle (die ihrerseits den FDS-Ancestor-Chain bis zur Workspace-Wildcard hochlaeuft).
Die Liste kann konstruktionsbedingt nur "explicit True" ausdruecken, kein "explicit False". Ein Tabellen-Toggle wirkt deshalb cascade-reset-mäßig auf alle Felder via Inheritance: der `setFlag`-Pfad wischt `neutralizeFields` bei expliziter Tabellen-Aenderung leer (siehe Cascade-Reset-Abschnitt), sodass alle Felder anschliessend den neuen Tabellen-Wert sehen.
`FdsField` traegt nur `neutralize` (keine `scope`, keine `ragIndexEnabled`). Mixed-Aggregation auf der Tabelle erfolgt korrekt, weil `FdsTableNode.getLogicalChildren(...)` die Felder dynamisch als logische Children einhaengt (`_wireTableFieldsAsLogicalChildren` in `_buildTree.py`); divergierende Field-Walks (einige True via Override, andere False via Inherit) liefern `'mixed'` auf der Tabelle.
## RBAC fuer Flag-Aenderungen
Implementiert in `UdbNode.canEdit(context, rootIf)`. Pro Subklasse:
- `SyntheticContainerNode`, `MandateGroupNode`: nie editierbar.
- `_DataSourceFamilyNode` (Connection / Service / Folder / File): `rec.userId == context.user.id` wenn ein `DataSource`-Record existiert. Fuer **virtuelle Nodes** (Browse-Folder ohne Record) prueft `canEdit` stattdessen, ob die `UserConnection` dem User gehoert (`_isConnectionOwner`); `setFlag` erstellt dann automatisch einen DataSource-Stub-Record. Geteilte Sources (anderer User) bleiben read-only.
- `_FdsFamilyNode` (FdsWorkspace / FdsTable / FdsRow), `FdsFieldNode`: `_isFeatureAdmin(rootIf, userId, featureInstanceId)` — verlangt eine `FeatureAccessRole` auf der Instanz, deren `Role.roleLabel` mit `-admin` endet (z.B. `workspace-admin`, `trustee-admin`). SysAdmin und PlatformAdmin haben **keine** automatische Erlaubnis (UDB-Edits sind ein Daten-Verantwortungs-Akt, nicht ein Plattform-Operations-Akt).
Globaler Scope (`scope="global"`) setzt zusaetzlich `context.isSysAdmin` voraus; siehe `_validateFlagValue` in `routeUdb.py`.
## Visibility
Visibility (was der User sieht) ist getrennt von Editability (was er aendern darf):
| Tab / Bereich | Sichtbar fuer den User |
|---|---|
| Chats | Chat-Workflows der Feature-Instanz, in der die UDB eingebettet ist |
| Folders + Files | Eigene Files/Folders + mit dem User geteilte (gemaess `scope`-Attribut der jeweiligen Source); fremde Attribute read-only |
| Personal Sources | Eigene Connections + Sources mit `scope in {mandate, global}` anderer User (read-only) |
| Feature Data Sources | Alle FDS der Feature-Instanzen im Mandanten, in denen der User RBAC-Zugriff hat |
Die Vererbung folgt **dem Path-Tree der jeweiligen Source**, nicht dem Owner. Sharing aendert daran nichts (die Berechtigungsmatrix der externen Provider — SharePoint, Drive, etc. — bleibt unberuehrt; UDB ueberlagert sie nicht).
## API
### `POST /api/udb/tree/children`
```
Body: { "parents": [null, "<key1>", "<key2>", ...] }
Response: { "nodesByParent": { "__root__": [...], "<key1>": [...], ... } }
```
`null` markiert das Top-Level. Jeder zurueckgegebene Node enthaelt:
```
key, kind, parentKey, label, icon, hasChildren,
dataSourceId, modelType,
effectiveNeutralize, effectiveScope, effectiveRagIndexEnabled,
supportsRag, canBeAdded,
+ kind-spezifische Carrier (authority, connectionId, service, sourceType,
path, featureInstanceId, featureCode, mandateId, tableName, objectKey,
fieldName, neutralizeFields)
```
Pre-computed effective Werte sind `boolean | "mixed"` bzw. `string | "mixed"`. FDS-Nodes haben keinen `effectiveScope`-Wert. `FdsFieldNode` traegt nur `effectiveNeutralize`.
### `POST /api/udb/node/{nodeKey}/flag/{flag}`
```
Path: nodeKey ∈ Tree-Keys (URL-encoded; '|' bleibt nach decodieren erhalten)
flag ∈ {neutralize, scope, ragIndexEnabled}
Body: { "value": <bool | "personal" | "mandate" | "featureInstance" | "global" | null> }
Response: { "nodeKey", "flag", "value", "effective", "resetDescendantIds" }
```
Ablauf:
1. `buildNodeForKey(nodeKey, ...)` parsed den Key in die zustaendige `UdbNode`-Subklasse.
2. `node.supportsFlag(flag)` → 400 wenn nicht.
3. `node.canEdit(context, rootIf)` → 403 wenn nicht.
4. `node.setFlag(flag, value, rootIf)` persistiert + cascade-reset, liefert die Liste der zurueckgesetzten Descendant-Ids.
5. `_computeEffectiveAfterWrite(...)` berechnet den frischen effective Wert (inkl. mixed).
6. Audit-Log via `audit_logger.logEvent(action="udb_flag_changed", ...)`.
`value=null` setzt den eigenen Wert auf `null` (vererbe wieder) **ohne** Cascade-Reset und ohne Index-Purge.
### Key-Format
| Pattern | Beschreibung |
|---|---|
| `personalRoot` | Top-Level-Container der eigenen Connections |
| `mgrp\|<mandateId>` | Mandanten-Kopfknoten |
| `conn\|<connId>` | UserConnection-Root |
| `svc\|<connId>\|<service>` | Service unter einer Connection (sharepoint, outlook, drive, ...) |
| `ds\|<connId>\|<sourceType>\|<path>` | Folder oder File innerhalb eines Services |
| `feat\|<mandateId>\|<featureCode>\|<fiId>` | Feature-Instanz (Workspace-Wildcard `*`) |
| `fdstbl\|<fiId>\|<tableName>` | Feature-Datentabelle |
| `fdsfld\|<fiId>\|<tableName>\|<fieldName>` | Virtuelles Feld unter einer Table |
`buildNodeForKey(...)` ist die einzige Stelle, die Keys decodiert; alle anderen Stellen erhalten bereits getypte `UdbNode`-Instanzen.
## Frontend-Vertrag
`ui-nyla/src/components/UnifiedDataBar/UdbSourcesProvider.tsx`:
- Einziger Cache: `Map<key, UdbBackendNode>` plus expanded-Set.
- `loadChildren(parent)``POST /api/udb/tree/children`.
- `patchScope` / `patchNeutralize` / `patchRagIndex``POST /api/udb/node/{key}/flag/{flag}` mit `{value}`-Body.
- Keine Vererbungs- oder Aggregations-Logik im Frontend.
- Pro Toggle: Spinner auf dem Flag-Button → API-Call → Refetch der betroffenen Parents → Re-Render. Keine optimistic Updates.
- Einheitliches mixed-Symbol fuer alle drei Flags (`◩`, U+25E9).
- Klick auf das mixed-Symbol → setzt explizit `false` (gilt fuer alle drei Flags). Reset auf `null`/inherit erfolgt ausschliesslich durch Parent-Toggle (siehe Cascade-Reset).
## Schluessel-Dateien
| Bereich | Pfad |
|---|---|
| **Polymorphe Node-Klassen** | `platform-core/modules/serviceCenter/services/serviceKnowledge/udbNodes.py` |
| **Tree-Builder** | `platform-core/modules/serviceCenter/services/serviceKnowledge/_buildTree.py` |
| **Inheritance-Helper** | `platform-core/modules/serviceCenter/services/serviceKnowledge/_inheritFlags.py` |
| **Generic Router** | `platform-core/modules/routes/routeUdb.py` |
| **DataSource-Model** | `platform-core/modules/datamodels/datamodelDataSource.py` |
| **FDS-Model** | `platform-core/modules/datamodels/datamodelFeatureDataSource.py` |
| **Frontend-Provider** | `ui-nyla/src/components/UnifiedDataBar/UdbSourcesProvider.tsx` |
| **Frontend-Rendering** | `ui-nyla/src/components/UnifiedDataBar/SourcesTab.tsx` |
| **Backend-Tests** | `platform-core/tests/unit/services/test_udbNodes.py`, `test_buildTree.py`, `test_inheritFlags.py` |
| **Frontend-Tests** | `ui-nyla/src/components/UnifiedDataBar/__tests__/UdbSourcesProvider.test.ts` |
## Regeln / Invarianten
- **Polymorphismus statt `if kind == ...`.** Neue Node-Typen erweitern die Klassen-Hierarchie; sie aendern nicht den Router oder den Builder.
- **Backend liefert effective Werte.** Das Frontend rendert sie 1:1. Wenn das UI „falsch“ aussieht, liegt der Fehler im Backend (Builder oder Resolver), nicht in der UI.
- **Eine API pro Verantwortung.** `tree/children` fuer Sichtbarkeit, `node/{k}/flag/{f}` fuer Persistenz. Keine Spezialrouten pro Flag oder pro Kind.
- **Hart-Cut bei Refactors.** Alte Endpoints und alter Frontend-Code werden geloescht, nicht parallel gehalten. Tests werden mitgezogen.
- **FDS hat keinen Scope und kein `userId`.** Wer das einbauen will, baut ein neues, separates Domain-Konzept — keine Vermischung.
- **Audit ist Pflicht.** Jede `setFlag`-Ausfuehrung schreibt einen `udb_flag_changed`-Eintrag mit `nodeKey`, `flag`, `value`, `resetDescendants`, `nodeKind`.

View file

@ -0,0 +1,141 @@
<!-- status: validate -->
<!-- started: 2026-05-23 -->
<!-- component: platform-core | ui-nyla -->
<!-- lastReviewed: 2026-05-23 -->
# UDB Polymorphic Refactor (Object-orientiert + Generic Router)
## Beschreibung und Kontext
Nach dem Generic-Tree-Refactor (`2026-05-udb-generic-tree-refactor.md`) blieben strukturelle Schwaechen, die in der Praxis zu wiederholten "stuck toggle"-Bugs gefuehrt haben:
- Die Flag-Logik war ueber `_buildTree.py`, `_inheritFlags.py`, vier PATCH-Routen, zwei Aggregatoren und das Frontend verstreut. Jede neue Node-Familie verlangte `if kind == ...`-Verzweigungen an mehreren Stellen.
- `FeatureDataSource` trug ein `userId`, ein `workspaceInstanceId` und ein `scope`-Feld, obwohl FDS feature-owned ist und Scope ueber RBAC abgedeckt sein muss. Das verfuehrte zur Vermischung von Mandate-Visibility und User-Sharing.
- Die UDB war ueber die `workspace`-Route ans Workspace-Feature verdrahtet, obwohl sie domaenenfrei und auf Systemebene nutzbar sein soll.
- Ein Bug, bei dem `neutralize=true` auf einem DB-Feld nicht mehr zurueckschaltbar war, liess sich nicht punktuell beheben — Root Cause war fragmentierte Logik, nicht ein einzelnes Codestueck.
User-Direktive: "keine optimistic hacks, keine workarounds und fallbacks, klare Logik, wo wir Fehler auch sehen, nicht verdecken". Ergo: harter Schnitt, polymorphes Backend, ein generischer Router.
## Ziele
1. **Polymorph statt fragmentiert.** Jeder Node-Typ ist eine Klasse mit klaren Hooks (`canEdit`, `getEffectiveFlag`, `setFlag`, `getLogicalChildren`, `toDict`). Neue Kinds erweitern die Hierarchie statt zentrale Switches.
2. **Eine generische API.** `POST /api/udb/tree/children` fuer Walks, `POST /api/udb/node/{key}/flag/{flag}` fuer Persistenz. Adressiert via Tree-Key (funktioniert auch fuer virtuelle Nodes wie `fdsField`).
3. **UDB feature-entkoppelt.** Eigene `/api/udb`-Route. Keine Bindung mehr ans Workspace-Feature.
4. **Klare Ownership-Semantik.** DataSource = user-private mit `scope`. FeatureDataSource = feature-owned, RBAC-gated, kein `userId`/`workspaceInstanceId`/`scope`.
5. **RBAC explizit verankert.** Flag-Edits auf FDS verlangen Feature-Admin (`Role.roleLabel.endswith("-admin")` auf der `featureInstanceId`). SysAdmin/PlatformAdmin sind kein Bypass.
## Nicht-Ziele
- Kein Kompatibilitaets-Layer. Alte Endpoints werden geloescht, nicht gespiegelt.
- Keine Aenderung an der UDB-UI-Optik (Symbole, Klick-Semantik, mixed-Anzeige) — diese stammt aus dem vorherigen Refactor.
- Keine Erweiterung der RAG-Pipeline. Bootstrap-Job laeuft weiter, jetzt aber per `featureInstanceId` (FDS hat keinen `workspaceInstanceId` mehr).
## Betroffene Module
- **Platform-Core (Backend):**
- Neu: `modules/serviceCenter/services/serviceKnowledge/udbNodes.py` (Klassenhierarchie)
- Neu: `modules/routes/routeUdb.py` (Generic Router)
- Refactored: `modules/serviceCenter/services/serviceKnowledge/_buildTree.py` (Builder geben `UdbNode`-Instanzen zurueck)
- Refactored: `modules/serviceCenter/services/serviceKnowledge/_inheritFlags.py` (FDS-Vererbung ohne `scope`, Coordinate `featureInstanceId` statt `workspaceInstanceId`)
- Refactored: `modules/datamodels/datamodelFeatureDataSource.py` (entfernt: `userId`, `workspaceInstanceId`, `scope`)
- Refactored: `modules/routes/routeDataSources.py` (4 PATCH-Endpoints entfernt; nur `/settings` und `/cost-estimate` bleiben)
- Refactored: `modules/features/workspace/routeFeatureWorkspace.py` (alter `/tree/children`-Endpoint entfernt; FDS-Create ohne `userId`/`workspaceInstanceId`)
- Refactored: `modules/routes/routeRagInventory.py` (Reindex via `featureInstanceId`)
- Refactored: `modules/serviceCenter/services/serviceKnowledge/subFeatureBootstrap.py` (Payload `featureInstanceId`, kein FDS-`userId` mehr → `"system"`)
- Refactored: `modules/serviceCenter/services/serviceAgent/coreTools/_featureSubAgentTools.py` (FDS-Lookup ohne `workspaceInstanceId`)
- App-Registration: `app.py` (`app.include_router(udbRouter)`)
- **UI-Nyla (Frontend):**
- Refactored: `src/components/UnifiedDataBar/UdbSourcesProvider.tsx` (alle Flag-Patches gehen ueber `POST /api/udb/node/{key}/flag/{flag}`, `_patchFieldNeutralize` entfernt)
- Refactored: `src/api/connectionApi.ts` (alte `patchDataSourceRagIndex`-Helper entfernt)
- Docstring: `src/components/UnifiedDataBar/SourcesTab.tsx`
- **Tests:**
- Neu: `platform-core/tests/unit/services/test_udbNodes.py` (umfassende Coverage der Klassenhierarchie)
- Refactored: `platform-core/tests/unit/services/test_buildTree.py`
- Refactored: `platform-core/tests/unit/services/test_inheritFlags.py`
- Refactored: `ui-nyla/src/components/UnifiedDataBar/__tests__/UdbSourcesProvider.test.ts`
- **Wiki:**
- Neu: `wiki/b-reference/platform/unified-data-bar.md` (kanonische Reference)
- Updates: `wiki/b-reference/platform/neutralization.md`, `wiki/b-reference/platform/rbac.md`, `wiki/TOPICS.md`, `wiki/c-work/_CHANGELOG.md`
- **DB-Migration:** nein (Schema-Felder am Model entfernt; bestehende DB-Spalten bleiben unbenutzt liegen — keine Datenverluste, keine Live-Migration noetig).
## Entscheidungen
| Datum | Entscheidung | Begruendung |
|---|---|---|
| 2026-05-23 | Hart-Cut der 4 PATCH-Routen | "Keine Workarounds, keine optimistic hacks." User-Direktive. |
| 2026-05-23 | Tree-Key als API-Adresse statt `sourceId` | Funktioniert auch fuer virtuelle Nodes wie `fdsField` (kein eigener DB-Record). |
| 2026-05-23 | Eigene `/api/udb`-Route, weg von `/api/workspace` | UDB ist feature-unabhaengig; sie kann auch auf System-Level ohne Feature-Kontext verwendet werden. |
| 2026-05-23 | FDS verliert `userId`, `workspaceInstanceId`, `scope` | FDS ist feature-owned. Visibility kommt aus RBAC, Edit aus Feature-Admin. Scope-Vermischung erzeugte den Field-Toggle-Bug. |
| 2026-05-23 | RBAC fuer FDS-Edits: `roleLabel.endswith('-admin')` | Pro Feature-Modul gibt es eigene Admin-Rollen (`workspace-admin`, `trustee-admin`, ...). Suffix-Match statt Allow-List. |
| 2026-05-23 | Kein SysAdmin/PlatformAdmin-Bypass auf UDB-Flag-Edits | Daten-Verantwortungs-Akt, kein Plattform-Operations-Akt. |
| 2026-05-23 | Visibility-Cascade fuer geteilte DataSources nicht eingebaut | Wuerde die Berechtigungsmatrix der externen Provider (SharePoint, Drive) ueberlagern. Sharing bleibt strikt read-only fuer Empfaenger. |
## Architektur (Kurzfassung)
```mermaid
flowchart LR
Frontend["UdbSourcesProvider.tsx"] -->|"POST /api/udb/tree/children"| Router["routeUdb.py"]
Frontend -->|"POST /api/udb/node/{k}/flag/{f}"| Router
Router -->|"buildNodeForKey(k)"| Nodes["udbNodes.py - UdbNode + Subklassen"]
Router -->|"getChildrenForParents(...)"| Builder["_buildTree.py"]
Builder --> Nodes
Nodes --> InheritFlags["_inheritFlags.py - Path-Traversal + Cascade"]
Nodes --> Models["DataSource / FeatureDataSource"]
```
Volle Architektur und API-Details: `b-reference/platform/unified-data-bar.md`.
## Umsetzungs-Checkliste
- [x] `udbNodes.py`: `UdbNode` ABC + Subklassen (synthetic, DS-Familie, FDS-Familie, FdsField)
- [x] `_aggregateChildrenToParents` geloescht (Logik jetzt polymorph in `getEffectiveFlag(mode=aggregate)`)
- [x] `_buildTree.py`: Builder geben `UdbNode`-Instanzen zurueck; `getChildrenForParents` ohne `instanceId`-Parameter
- [x] `routeUdb.py`: `/api/udb/tree/children` + `/api/udb/node/{key}/flag/{flag}`
- [x] Alte PATCH-Routen geloescht (`/scope`, `/neutralize`, `/rag-index`, `/neutralize-fields`); alter `/api/workspace/{id}/tree/children` geloescht
- [x] `FeatureDataSource`-Schema: `userId`, `workspaceInstanceId`, `scope` entfernt
- [x] Frontend-Provider auf `/api/udb/*` umgestellt
- [x] Backend-Tests: `test_udbNodes.py` neu, `test_buildTree.py` + `test_inheritFlags.py` angepasst
- [x] Frontend-Tests: `UdbSourcesProvider.test.ts` angepasst
- [x] Wiki: neue Reference-Page `unified-data-bar.md`; Updates auf `neutralization.md`, `rbac.md`, `TOPICS.md`, `_CHANGELOG.md`
- [ ] Runtime-Smoke: lokal Backend starten, je einen Toggle pro Familie testen (`fdsField`, `fdsTable`, `ds-folder`, `connection`)
## Akzeptanzkriterien
| # | Kriterium (Given-When-Then) | Prio |
|---|---|---|
| 1 | Given ein DS-File-Node mit explizitem `neutralize=true` und ein Parent-Folder ohne expliziten Wert, When der User auf das mixed-Symbol des Parent klickt, Then setzt das Backend `neutralize=false` am Parent und cascade-resetted die expliziten Werte aller Descendants. | must |
| 2 | Given ein User ohne `-admin`-Rolle auf der Feature-Instanz, When er versucht `neutralize` auf einer FDS-Table zu togglen, Then antwortet der Server mit 403 und schreibt einen Audit-Eintrag. | must |
| 3 | Given eine FDS mit `neutralizeFields=["email"]`, When der User auf das Feld `email` klickt und neutralize toggelt, Then wird `email` aus der Liste entfernt (`neutralizeFields=[]`) und der naechste Refetch zeigt `effectiveNeutralize=false` auf dem Field. | must |
| 4 | Given drei Files unter demselben Folder, alle mit explizitem `neutralize=true`, When der Folder via Refetch geladen wird, Then liefert das Backend `effectiveNeutralize=true` (nicht "mixed") am Folder. | must |
| 5 | Given zwei Files unter demselben Folder, eines mit `neutralize=true` und eines mit `neutralize=false`, When der Folder via Refetch geladen wird, Then liefert das Backend `effectiveNeutralize="mixed"` am Folder. | must |
| 6 | Given ein POST auf `/api/udb/node/{k}/flag/scope` mit `value="global"` von einem nicht-SysAdmin, When der Request validiert wird, Then 403 und kein DB-Write. | must |
## Testplan
| ID | AC | Art | Automatisiert | Repo-Pfad | Status |
|----|----|-----|---------------|-----------|--------|
| T1 | 1,4,5 | unit | ja | `platform-core/tests/unit/services/test_udbNodes.py::TestGetEffectiveFlag` | done |
| T2 | 2 | unit | ja | `platform-core/tests/unit/services/test_udbNodes.py::TestCanEditFdsFeatureAdmin`, `TestIsFeatureAdmin` | done |
| T3 | 3 | unit | ja | `platform-core/tests/unit/services/test_udbNodes.py::TestSetFlag` (FdsFieldNode-Branch) | done |
| T4 | 1 | unit | ja | `platform-core/tests/unit/services/test_inheritFlags.py` (cascade-reset) | done |
| T5 | 6 | manuell | nein | Runtime-Smoke via UI/curl | pending |
| T6 | alle | runtime | nein | Lokaler Smoke-Test mit `ui-nyla` + `platform-core` | pending |
## Risiken / offene Punkte
- **DB-Spalten am `FeatureDataSource` bleiben physisch in der Tabelle liegen** (`userId`, `workspaceInstanceId`, `scope`). Solange die ORM-Modelle sie nicht mehr referenzieren, sind sie inert. Eine spaetere DROP-Migration ist optional.
- **Runtime-Smoke pendet.** Vor dem Verschieben in `4-done` ist mindestens ein End-to-End-Test pro Node-Familie erforderlich (Edit + Read-back via UI).
## Links
- Reference: `b-reference/platform/unified-data-bar.md`
- Vorgaenger: `c-work/4-done/2026-05-udb-generic-tree-refactor.md`, `c-work/4-done/2026-05-udb-cascade-inherit.md`
- Toggle-Spec-Recovery (Symptome, die den Refactor erzwungen haben): `c-work/3-validate/2026-05-udb-toggle-spec-recovery.md`
## Abschluss
- [ ] Runtime-Smoke gruen
- [x] `b-reference/platform/unified-data-bar.md` angelegt
- [x] `TOPICS.md` Eintrag ergaenzt
- [x] `_CHANGELOG.md` Eintrag ergaenzt
- [ ] Dieses Dokument → `c-work/4-done/` verschieben (nach Runtime-Smoke)

View file

@ -0,0 +1,85 @@
<!-- status: validate -->
<!-- started: 2026-05-26 -->
<!-- component: platform-core | ui-nyla -->
<!-- lastReviewed: 2026-05-26 -->
# UDB Toggle Spec Recovery
## Kontext
Nach dem Generic Tree Refactor (2026-05-18) waren im Code sechs Aggregations-Routinen
(`_aggregatePersonalRoot`, `_aggregateConnection`, `_aggregateMandateGroup`,
`_resolveAttrsForKey`, `getAttributesForKeys`), ein paralleler Endpoint
`POST /tree/attributes` und ein Bug bei virtuellen Coordinates (kein aggregate
fuer Records ohne DB-Eintrag) entstanden. Das Frontend nutzte `refreshAttributes`
als optionalen Pfad und `refreshAfterAction` als Opt-in-Prop, obwohl die Spec
nur einen Pipeline-Pfad vorsieht.
## Ziel
Wiederherstellung der dokumentierten Spec (2026-05-18 "Generic Tree Refactor"):
**Eine** Pipeline, **ein** Resolver, **ein** Mixed-Symbol, **drei** Flags
generisch durch denselben Code-Pfad. Keine optimistic updates, keine
Fallbacks, keine doppelte Logik.
## Durchgefuehrte Aenderungen
### S1 - Backend: virtuelle Coordinates auf aggregate-Pfad
`_inheritFlags.py`: `resolveEffectiveForPath` und `resolveEffectiveForFds` rufen
im else-Zweig (virtual record, kein DB-Match) nun `getEffectiveFlag(virtualRec, flag, allDs, mode=mode)`
bzw. `getEffectiveFlagFds(...)` statt `_resolveWalkValue` auf. Damit koennen
virtuelle Coordinates (z.B. Connection ohne eigenen Root-Record) korrekt
`'mixed'` zurueckgeben, wenn ihre Descendants divergieren.
### S2 - Backend: sechs Aggregatoren entfernt
`_buildTree.py`: Geloescht: `_aggregatePersonalRoot`, `_aggregateConnection`,
`_aggregateMandateGroup`, `_resolveAttrsForKey`, `getAttributesForKeys`.
Diese waren ausschliesslich fuer den parallelen `tree/attributes`-Endpoint noetig.
Synthetic Container (`personalRoot`, `mgrp`) haben laut Spec keine Flags
(UI blendet sie aus); data-bearing Nodes nutzen bereits
`resolveEffectiveForPath`/`resolveEffectiveForFds` via den tree builder.
### S3 - Backend: Endpoint tree/attributes entfernt
`routeFeatureWorkspace.py`: Route `POST /{instanceId}/tree/attributes` und
Model `_TreeAttributesRequest` ersatzlos geloescht.
### S4 - Frontend: refreshAttributes-Pfad entfernt
- `UdbSourcesProvider.tsx`: `refreshAttributes`-Methode entfernt.
- `FolderFileProvider.tsx`: `refreshAttributes`-Methode entfernt.
- `FormGeneratorTree.tsx`: `_refreshVisibleAttributes` nutzt nur noch den
Refetch-All-Expanded-Pfad. `_runAction` ruft immer `_refreshVisibleAttributes`
ohne Bedingung. `refreshAfterAction`-Prop entfernt.
- `types.ts`: `refreshAttributes` aus `TreeNodeProvider`, `refreshAfterAction`
aus `FormGeneratorTreeProps` entfernt.
- `SourcesTab.tsx`, `FilesTab.tsx`: `refreshAfterAction`-Prop-Nutzung entfernt.
### S5 - Klick-Semantik dokumentiert
`wiki/b-reference/platform/neutralization.md`: Klick auf mixed-Symbol setzt
explizit `false` (alle Flags). Begruendung: Toggle behaelt zwei klare
End-Zustaende; Reset auf `null`/inherit durch Parent-Toggle.
### S6 - Tests
8 neue Tests in `test_inheritFlags.py::TestVirtualCoordAggregate`:
- virtual folder mixed neutralize/scope/rag
- virtual folder uniform returns concrete
- virtual FDS workspace mixed/uniform
- virtual connection root mixed via diverging services
Frontend-Test `refreshAfterAction` angepasst: zweiter Test (does NOT refetch
when false) entfernt, da Verhalten nun immer refetch ist.
Resultat: `test_inheritFlags.py` 79/79 gruen, `test_buildTree.py` 19/19 gruen.
## Validierung (Smoke-Tests vor Merge)
1. Folder mit zwei Subfolders, einer toggeln -> Parent zeigt mixed, Root zeigt mixed, Personal Root zeigt mixed.
2. Parent toggeln (war mixed) -> Parent + alle Descendants zeigen den Parent-Wert.
3. Klick auf mixed-Symbol -> setzt explizit `false`, Spinner bleibt bis Refetch durch ist.
4. Toggle in collapsed Subtree (verstecktem Knoten) -> nach expand zeigt Subtree die neuen Werte.
5. Mandate mit zwei Workspaces, einer toggeln -> mgrp zeigt mixed.

View file

@ -156,3 +156,17 @@ Frontend: `npx tsc --noEmit --skipLibCheck` clean.
- Backend: `gateway/modules/serviceCenter/services/serviceKnowledge/_buildTree.py`, `_inheritFlags.py`, `gateway/modules/features/workspace/routeFeatureWorkspace.py`, `gateway/modules/routes/routeDataSources.py`, `gateway/modules/datamodels/datamodelFeatureDataSource.py`
- Frontend: `frontend_nyla/src/components/UnifiedDataBar/SourcesTab.tsx`
- Tests: `gateway/tests/unit/services/test_buildTree.py`, `test_inheritFlags.py`
## Nachtrag 2026-05-26: Spec Recovery
Im Code waren nach dem Refactor sechs Aggregations-Routinen (`_aggregatePersonalRoot`,
`_aggregateConnection`, `_aggregateMandateGroup`, `_resolveAttrsForKey`,
`getAttributesForKeys`) und ein paralleler Endpoint `POST /tree/attributes`
entstanden, die der oben dokumentierten Single-Pipeline-Architektur widersprachen.
Zusaetzlich fehlte bei virtuellen Coordinates (Records ohne DB-Eintrag) die
Subtree-Aggregation, sodass `'mixed'` nie zurueckgegeben werden konnte.
Das Frontend nutzte `refreshAttributes` und `refreshAfterAction` als optionale
Pfade statt des einen Refetch-All-Expanded-Pfads.
Diese Abweichungen wurden am 2026-05-26 zurueckgefuehrt
(siehe `c-work/3-validate/2026-05-udb-toggle-spec-recovery.md`).

View file

@ -12,12 +12,23 @@ type: `feat` `fix` `refactor` `docs` `test` `chore` `build` · scope: `gateway
Skip: reine Refactors, Formatting, Lint, Dep-Bumps, Test-only, Wiki-Tippfehler.
## 2026-05-27
- 2026-05-27 | fix | platform-core | UDB DS Auto-Create: Virtuelle DataSource-Nodes (Browse-Folder ohne DB-Record) auf eigener Connection koennen jetzt getoggelt werden. `canEdit` prueft Connection-Ownership via `UserConnection.userId`; `setFlag` erstellt automatisch einen DataSource-Stub-Record analog zu `_findOrCreateTableFds`. Behebt 403-Errors beim Toggling von Browse-Foldern. 4 neue Tests (`test_udbNodes.py`).
- 2026-05-27 | refactor | platform-core | UDB neutralizeFields-Handling von generischer Infrastruktur in polymorphe Node-Klassen verschoben: `_inheritFlags.cascadeResetDescendantsFds` ist wieder rein generisch (keine `neutralizeFields`-Logik); `_FdsFamilyNode.setFlag` bietet generischen `_onSetFlag`-Hook; `FdsTableNode._onSetFlag` wipe-t eigene `neutralizeFields` bei explizitem neutralize-Toggle; `FdsWorkspaceNode._onSetFlag` wipe-t `neutralizeFields` auf allen Descendant-Tables. `FdsFieldNode.getEffectiveFlag` erbt korrekt vom Table (Zwei-Quellen-Modell). 122/122 Tests gruen. Wiki `unified-data-bar.md` (Cascade-Reset-Abschnitt) aktualisiert.
## 2026-05-26
- 2026-05-26 | fix | gateway+frontend-nyla | UDB Toggle Spec Recovery: virtuelle Coordinates nutzen neu aggregate-Pfad (mixed moeglich ohne DB-Record); sechs redundante Aggregatoren + Parallel-Endpoint tree/attributes + Frontend refreshAttributes-Pfad entfernt; eine Pipeline, ein Resolver, immer refetch nach Toggle (Spec 2026-05-18 wiederhergestellt) (c-work: 3-validate/2026-05-udb-toggle-spec-recovery.md)
## 2026-05-24
- 2026-05-24 | docs | infra | **Deployment-Infrastruktur Doku** -- Neue kanonische Seite `b-reference/platform/infrastructure.md`: Infomaniak-Projektstruktur (Porta, LLM, Teamsbot), Naming Convention (`{bereich}-{env}-{komponente}`), VM-Instanzenliste, Deploy-Patterns. TOPICS.md ergaenzt.
## 2026-05-23
- 2026-05-23 | refactor | platform-core+ui-nyla+wiki | UDB Polymorphic Refactor: neue `UdbNode`-Klassenhierarchie (`udbNodes.py`), generischer Router `routeUdb.py` mit `POST /api/udb/tree/children` + `POST /api/udb/node/{key}/flag/{flag}`, Hart-Cut der 4 PATCH-Routen + altem `/api/workspace/{id}/tree/children`. FDS-Schema entschlackt (kein `userId`, kein `workspaceInstanceId`, kein `scope` mehr — feature-owned, RBAC-gated via `roleLabel.endswith('-admin')`). Neue kanonische Doku `b-reference/platform/unified-data-bar.md`; Updates auf `neutralization.md`, `rbac.md`, `TOPICS.md`. Tests: neu `test_udbNodes.py`, angepasst `test_buildTree.py` + `test_inheritFlags.py` + `UdbSourcesProvider.test.ts` (alle gruen) (c-work: 3-validate/2026-05-udb-polymorphic-refactor.md)
- 2026-05-23 | feat | gateway+frontend | DB Migration Progress: Export und Import laufen per-DB sequentiell mit Echtzeit-Fortschrittslog (Timestamp, DB-Name, Tabellen-/Datensatzzahl, Fehler); neuer export-single Endpoint; Import via prepare-import + import-single (server-seitiger Payload-Cache mit Token); Export-Dateiname enthaelt Instanzlabel + full/partial (z.B. db_backup_main_full_2026-05-23T06-20-54.json)
- 2026-05-23 | feat | gateway+frontend-nyla | **DB Migration Backup/Restore** — Neuer Tab "Migration" auf der Datenbank-Gesundheit-Seite (SysAdmin). Backup: dynamische DB-Auswahl via Registry, Export als JSON. Restore: JSON-Upload, Validierung, Import mit Modus "Neu" (replace) oder "Zusammenfuehren" (merge). System-Objekte (Root-Mandant, Admin-User, Event-User) werden nie geloescht; ihre IDs werden beim Import automatisch auf die Live-IDs remapped. Neue Dateien: `databaseMigration.py`, 4 API-Endpoints unter `/api/admin/database-health/migration/`, MigrationTab in `AdminDatabaseHealthPage.tsx`.
## 2026-05-22
@ -27,11 +38,18 @@ Skip: reine Refactors, Formatting, Lint, Dep-Bumps, Test-only, Wiki-Tippfehler.
## 2026-05-19
- 2026-05-19 | fix | gateway | RAG Inventory /mandate: ImportError UserMandate behoben (war in datamodelUam statt datamodelMembership); refactored zu getUserMandatesByMandate(); RBAC membership-Check hinzugefuegt (verhindert Zugriff auf fremde Mandate via X-Mandate-Id)
- 2026-05-19 | fix | gateway+frontend | RAG Inventory Mandate-Dropdown: neuer Endpunkt GET /api/rag/inventory/my-mandates liefert nur Mandate mit aktiver Mitgliedschaft; Frontend nutzt diesen statt /api/mandates/ (admin-only Route)
- 2026-05-19 | feat | gateway+frontend | RAG Inventory Feature-Daten: featureInstances-Array mit Datei-/Chunk-Zaehler, Status-Breakdown (indexed/pending/failed), FeatureDataSource-Details mit ragIndexEnabled-Flag, RAG-Aktiv-Indikator; Frontend zeigt Sync-Status-Banner, Datenquellen pro Instanz, RAG-Hinweis wenn inaktiv; leere Instanzen werden ausgefiltert
- 2026-05-19 | feat | gateway+frontend | Feature-Daten RAG-Sync: neuer Background-Job-Handler feature.bootstrap (subFeatureBootstrap.py) indexiert FeatureDataSource-Tabellendaten via FeatureDataProvider+requestIngestion; Handler-Registrierung in registerKnowledgeIngestionConsumer; neuer Endpoint POST /api/rag/inventory/reindex-feature/{workspaceInstanceId} mit FeatureAccess-Check; _buildFeatureInstanceInventory liefert runningJobs/lastSuccess/lastError; Frontend: Job-basierte Sync-Status-Banner + Reindex-Button fuer Feature-Instanzen (analog zu Connection-Sync)
- 2026-05-19 | fix | gateway | FeatureDataProvider: alle DB-Zugriffe (getActualColumns, browseTable, aggregateTable, queryTable, _resolveInstanceColumn) von deprecated db.connection.cursor() auf db.borrowCursor() migriert (Pool-API-Kompatibilitaet nach Pooling-Refactoring)
- 2026-05-19 | fix | gateway | Trustee Budget-Vergleich: _exportAccountingData liefert neu eine accountSummary (1 Zeile/Konto mit closingBalance + Q1-Q4) statt 5590 rohe Balance-Records (26/Konto); AI-Prompt praezisiert Datenquelle; reduziert Payload von 355KB auf 25KB und verhindert Faktor-100-Abweichungen durch versehentliches Summieren
- 2026-05-19 | fix | frontend-nyla+gateway | **RAG-Inventar Mandate-Scope 403 + SQL-Fehler** — Zwei Bugs: (1) Frontend sendete `mandateId` als Query-Parameter statt `X-Mandate-Id`-Header → `context.mandateId=None` → 403. Fix: Header wird gesetzt. (2) Backend `routeRagInventory._getInventoryMandate` filterte `UserConnection` mit `recordFilter={"mandateId": ...}`, aber `UserConnection` hat kein `mandateId`-Feld → SQL-Error. Fix: Mandate-Members via `UserMandate`-Junction holen, dann deren `getUserConnections(userId)` aggregieren.
- 2026-05-19 | refactor | gateway+frontend-nyla | **UDB Toggle Refresh: Visible-IDs Pattern** -- After a toggle, FE sends all visible node IDs in one POST request; backend returns correct attribute values (incl. mixed) for exactly those IDs. No full-tree reload, no optimistic updates. New endpoints: `POST /api/files/attributes` (FilesTab) and `POST /api/workspace/{id}/tree/attributes` (SourcesTab). `TreeNodeProvider.refreshAttributes(ids)` added to interface. Collapse now removes children from FE state (always fresh on re-expand). Removed `_enrichFoldersWithMixed` and `_refetchAllExpanded`.
## 2026-05-18
- 2026-05-18 | feat | gateway+frontend | UDB Resolve Endpoint: neuer POST /datasources/resolve-flags Bulk-Endpoint liefert effective-Werte (neutralize, scope, ragIndexEnabled) fuer beliebige Pfade auch ohne eigenen DB-Record; Frontend entfernt client-seitige Vererbungslogik (_findCoveringDs), nutzt ausschliesslich Backend-Daten via resolvedFlags Map
- 2026-05-18 | fix+feat | gateway+frontend-nyla | **UDB Sources Recovery -- Zweite Smoke-Test-Runde (H1-H10)** (`c-work/3-validate/2026-05-udb-sources-recovery.md`). (H1) Logik-Audit aller drei Flags zusammengefasst: `'mixed'` ist Backend-Aggregate-Anzeige, niemals persistiert; FE-Handler mappen `'mixed'` immer auf konkreten Wert (`'personal'`/`false`); Cascade-PATCH setzt alle Nachkommen auf `NULL` und dann den Master, deshalb kann aggregate nach einem User-Toggle nicht `'mixed'` bleiben. (H2/H8) Initial-Render-Bug: sequentieller `for ... await` im Auto-Expand-Effekt loeste Cancellation-Race aus -- erstes `setNodes` triggerte cleanup, weitere defaultExpanded-Knoten blieben "expanded ohne Children". Fix: `Promise.all(...)` parallel + atomares Single-`setNodes`. Zusaetzlich `autoExpandedRef.current.clear()` in `_loadRoot` (StrictMode-Doppelmount + manuelle Refresh tauglich). (H3) `PUT /api/files/{id}` 404: Route hatte keinen `RequestContext`, `interfaceDbManagement.getInterface(currentUser)` ohne mandate/featureInstance scope -> RBAC-Filter excludiert File. Fix: Context-Dep + scope-pass-through bei `update_file` und `delete_file`; `move_folder` akzeptiert sowohl `parentId` als auch `targetParentId` aus dem Body. (H4) Neuer Folder erschien initial auf Legacy-Top-Level: `FolderFileProvider.createChild(parentId=null, ...)` setzt jetzt `parentId = _SYNTH_ROOT_ID('own')` damit der neue Folder unter `/` rendert. (H5) `+`-Button pro Folder im FormGeneratorTree: neue `onCreateChild?: (parentId: string) => void`-Prop in TreeNodeRow + verdrahtetem `_createFolderAt(parentId)` Handler in der Hauptkomponente; sichtbar im Hover-Action-Slot fuer alle Folders mit `provider.createChild` && `provider.canCreate(id)`. (H6) FilesTab neutralize partly broken: Diagnose plausibel mit H2-Fix erledigt (Optimistic-Update verfehlte Knoten, deren Children durch das Cancellation-Race nicht im FE-State waren). (H7) Hardcoded `[Persoenliche Quellen]`: Source-String hatte `oe`-Encoding, `t()` returned key fuer DE -> sichtbar mit fake-Umlaut. Fix: `resolveTextSafe("Persoenliche Quellen")` -> `resolveTextSafe("Persönliche Quellen")` (echter Umlaut). (H9) FDS-Neutralize ging nicht: `POST /api/workspace/{instanceId}/feature-datasources` 403te Cross-Mandate-Erstellung (Workspace mandate A, Feature mandate B beides user-zugaenglich), `_ensureRecord` schluckte den Fehler still. Fix: Cross-Mandate-Block entfernt, statt dessen `getFeatureAccess(userId, body.featureInstanceId)`-Validierung; `mandateId` der neuen FDS = `wsMandateId` (= Workspace-Tenancy, konsistent mit Tree-Filter). (H10 = G5) Persistenter Expand-State umgesetzt: `WorkspaceUserSettings.uiTreeExpansion: Dict[str, List[str]]` Backend-Field; neue Routen `GET/PUT /api/workspace/{instanceId}/ui-tree-expansion/{scope}`; FE-Hook `useTreeExpansion(instanceId, scope)` mit 600 ms Debounce-PUT; `FormGeneratorTree` Props `expandedIds?: string[] | null` + `onExpandedIdsChange?: (ids) => void` (controlled mode); `SourcesTab` (scope `'sources'`), `FilesTab` (scopes `'filesOwn'` + `'filesShared'`) verdrahtet. Bei `null` (kein Record) Default-Verhalten + erster Toggle erstellt den Settings-Record; bei Array gewinnt persistierte Liste ueber Backend-`defaultExpanded`-Hints. Tests: Backend `services+routes` 107/107 gruen, Frontend UDB+FormGen 69/69 gruen.
- 2026-05-18 | fix+feat | gateway+frontend-nyla | **UDB Sources Recovery -- Smoke-Test-Followups (G1-G5)** (`c-work/3-validate/2026-05-udb-sources-recovery.md`). (G1) `tsconfig.json` referenziert jetzt auch `tsconfig.test.json`; Testfile bekam expliziten `import React`, `afterEach`, `@testing-library/jest-dom/vitest`. 139 -> 0 Lints; Tests 50/50 weiterhin gruen. (G2) Off-State der `neutralize`/`ragIndexEnabled`-Buttons: `filter: grayscale(1); opacity: .45` im OFF-Zustand -- offenes Schloss + greyed-Brain klar als "deaktiviert" lesbar. (G3a) Bug `_browseChildren`: Childs erbten `parentKey=ds|...|/` statt des aufgerufenen `svc|...`; Children erschienen daher nie im FE-Tree. Fix: `parentKey` als optionaler Parameter durchgereicht; Dispatcher uebergibt den asked-for-Key. (G3b) Per-Field-Neutralize fuer Feature-Tabellen: `fdsTable.hasChildren=True` bei vorhandener `meta.fields`-Liste; neuer Knoten-Typ `fdsField` (Dispatcher `fdstbl|fi|table` -> `_featureTableFields`). Effektiver Field-Neutralize = `parent.neutralize OR field IN parent.neutralizeFields`. Toggle: `UdbSourcesProvider.patchNeutralize` splittet die Batch nach Kind und ruft fuer fdsField `PATCH /api/datasources/{id}/neutralize-fields` mit der mutierten Liste; `neutralizeFields` ist im Tree-Payload mitgesendet -- kein Extra-GET. Scope/RAG auf Feldebene bewusst nicht editierbar. +3 neue Backend-Tests in `TestFeatureTableFields`. (G4) FilesTab synthetischer "/"-Root pro Ownership: Provider mappt `parentId=null` auf `[__filesRoot:<own|shared>]` (`defaultExpanded=true`); reale Top-Level-Items werden Children. `moveNodes` mit Synth-Root-Ziel re-mapt auf `parentId/folderId=null` (= Drop auf "/"); `patchScope`/`patchNeutralize` mit Synth-Root-Id materialisieren ueber API alle Top-Level-Folders+Files und setzen pro Folder `cascadeChildren=true` -- globaler Toggle ohne Backend-Aenderung. Tests: Backend 19/19 gruen, Frontend UDB+FormGen 69/69 gruen.
- 2026-05-18 | refactor | gateway+frontend-nyla | **UDB Sources Recovery -- Folge-UX-Iteration** (`c-work/3-validate/2026-05-udb-sources-recovery.md`). Auf das User-Feedback nach Phase 1-3: (a) **Visual cascade fuer `neutralize` und `ragIndexEnabled`** wieder klar erkennbar -- vorher *gleiches Symbol* fuer on/off (nur Opacity), Cascade unsichtbar. Distinct emojis: `_NEUTRALIZE_ON_EMOJI=closed lock` / `_NEUTRALIZE_OFF_EMOJI=open lock`, `_RAG_ON_EMOJI=brain` / `_RAG_OFF_EMOJI=thought-bubble`. Backend-Cascade war korrekt (parametrisch in `cascadeResetDescendants(rec, flag)`); rein UI-Issue. (b) **Top-Level-Layout flach**: synthetische Wrapper `srcRoot` + `mandateRoot` entfernt. `_topLevel` emittiert direkt `[personalRoot, mgrp|m1, mgrp|m2, ...]`. Mandate-Groups haben nun `parentKey=None`. (c) **`TreeNode.defaultExpanded?: boolean`** als generisches Tree-Feature (one-shot pro Node-Id ueber `autoExpandedRef`); BE markiert `personalRoot` und alle Mandate-Groups mit `defaultExpanded=True` -- UI oeffnet bis zur Datenquellen-Ebene ohne User-Klick. (d) **Icon-Reihenfolge** in `FormGeneratorTree` (rechtsbuendig, von rechts nach links): neutralize, scope, sendToChat, rag, settings. Settings (`extraActions`) jetzt linksbuendig. (e) **Settings-Icon nur auf Daten-Quellen-Root** (`kind in {connection, featureNode}`), aber dort *immer* sichtbar, auch ohne `dataSourceId`; Klick triggert lazy `_ensureRecord` -> `onOpenSettings`. (f) **`SourcesTab`** setzt `title={t('Datenquellen')}` als Section-Header. Tests: Backend `test_buildTree.py` 16/16 + `test_inheritFlags.py` 72/72 -> 88/88 gruen. Frontend `FormGeneratorTree.test.tsx` 50/50 (+`defaultExpanded`-Tests) + `UdbSourcesProvider.test.ts` 16/16 (+settings-on-root + defaultExpanded-passthrough). TypeScript clean.
@ -82,6 +100,8 @@ Skip: reine Refactors, Formatting, Lint, Dep-Bumps, Test-only, Wiki-Tippfehler.
## 2026-05-12
- 2026-05-12 | fix | service-teams-browser-bot | Auth bot PiP loop fix: in full Teams web app, chat toggle navigates to Chat section -> PiP -> periodic scan reopens -> PiP again (endless loop). Fix: added isAuthMode flag to ChatProcedure; in auth mode: skip chat panel toggle entirely, skip periodic reopen, skip panel-open preflight for sendChatMessage. Chat send uses direct input lookup in the meeting view. Replaced _returnToMeetingIfPip() approach (reactive) with prevention (don't toggle)
- 2026-05-12 | fix | service-teams-browser-bot | Auth bot meeting minimized (PiP): chat button click hit Teams sidebar "Chats" navigation instead of in-meeting toggle -> meeting minimized, all PeerConnections closed, video/audio broke. Fix: isDangerousNavButton filter excludes sidebar nav, tab-items, submenu triggers; aria-pressed fallback for panel detection in full Teams web app; _openMoreMenu scoped to calling toolbar
- 2026-05-12 | feat | gateway, frontend-nyla, teams-bot | Avatar-Bild/Video für TeamsBot: Neues Feld `avatarFileId` in `TeamsbotConfig`/`TeamsbotUserSettings` und `defaultAvatarFileId` in `TeamsbotMeetingModule`. Benutzer können in den Bot-Einstellungen und pro Modul ein Bild oder Video aus den Systemdateien auswählen, das anstelle der statischen Farbfläche als Bot-Video im Meeting gerendert wird. Modul-Einstellung überschreibt Instanz-Default. Gateway löst die Datei beim Session-Start auf, konvertiert zu Base64, und sendet sie im Join-Payload an den Browser-Bot. Bot-seitig: `mediaGetUserMediaPatch.ts` rendert Bilder per `drawImage()` auf den Canvas, Videos per `<video>`-Element mit Loop. Für Videos wird FPS auf 15 erhöht und `contentHint='motion'` gesetzt. Fallback bleibt die statische Farbfläche mit Bot-Name.
- 2026-05-12 | fix | gateway, frontend-nyla | TeamsBot Module-Status `[null]`: `createModule`-Route schrieb keinen `status` in die DB, weil `CreateMeetingModuleRequest` kein `status`-Feld hat und `recordCreate` bei Dict-Input keine Pydantic-Defaults anwendet. Frontend `t(null)` ergab `[null]`. Fix: (1) Route setzt `data.setdefault("status", "active")`, (2) `getModules` setzt fehlenden Status defensiv auf `"active"` für existierende Records, (3) Frontend-Fallback `mod.status || 'Aktiv'` als letzte Absicherung.
- 2026-05-12 | fix | frontend-nyla | TeamsBot Director-Panel Sichtbarkeit: gleiches Whitelist-Problem wie zuvor beim Stop-Button. `TeamsbotSessionView.tsx` Z.903 prüfte `['active','joining','pending'].includes(session.status)` — wenn der Status direkt nach Mount kurz leer / unbekannt war (SSE-Race vor `_loadSession`-Resolve, oder Backend schickt einen Übergangsstatus den die Liste nicht kennt), verschwand das ganze "Regieanweisungen"-Panel und kam erst nach Page-Reload wieder. Auf Blacklist umgestellt: `!['ended','error','leaving'].includes(session.status)`. Andere Whitelist-Stellen in derselben Datei (Session-Auswahl Z.136, SSE-Reconnect Z.186/373/439, Listen-Label Z.792) sind defensive Filter mit `status &&`-Guard und bleiben korrekt; Dashboard-Stop-Button war bereits auf Blacklist (Vorgänger-Fix).
@ -105,11 +125,22 @@ Skip: reine Refactors, Formatting, Lint, Dep-Bumps, Test-only, Wiki-Tippfehler.
## 2026-05-11
- 2026-05-11 | feat | gateway | ToolDefinition.displayLabel: neues Feld fuer menschenlesbare Aktivitaetsbeschreibungen; Agent-Loop propagiert Label in TOOL_CALL Events; TeamsBot nutzt es fuer kontextuelle Fortschrittsmeldungen statt "Schritt N von M"
- 2026-05-11 | fix | gateway | TeamsBot Speaker-Attribution: retroaktive Zuordnung von "Unknown"-Eintraegen erfolgt jetzt bei jedem neuen Caption-Speaker, nicht nur beim ersten
- 2026-05-11 | fix | service-teams-browser-bot | Chat-Scraper Root Cause: Timestamp-Separatoren ("22:01") wurden als Chat-Nachrichten erkannt weil der innerText-Fallback auch bei unbekanntem Autor griff; Fallback jetzt nur wenn Autor identifiziert wurde
- 2026-05-11 | fix | gateway | mainServiceWeb progressLogUpdate: fehlende `if operationId:` Guards bei Zeilen 101/125 verursachten "Operation None not found" Warnung wenn Agent-Tool webSearch ohne operationId aufrief
- 2026-05-11 | fix | gateway | SPEECH_TEAMS Prompt: spezifische Beispiele entfernt, Eskalationsregeln generisch gehalten
- 2026-05-11 | fix | service-teams-browser-bot | Anonymous bot stuck after lobby admission: _waitForMeetingAdmission now tracks lobby-to-meeting transition, waits patiently when lobby vanishes (= admitted), and reloads page as recovery; isInMeeting expanded with light-meetings + German selectors
- 2026-05-11 | docs | wiki | d-guides: `google-registration-checklist.md`, `microsoft-entra-registration-checklist.md` (Vendor-Registrierung + Gateway-Env/Code-Referenzen); TOPICS ergänzt.
- 2026-05-11 | feat | gateway, frontend-nyla, wiki | TeamsBot: `GET /api/teamsbot/{instanceId}/dashboard/stream` (SSE dashboardState 3s/20s); dashboard consumes EventSource + reconnect; module tiles navigate to `modules?moduleId=` with expand + scroll highlight; canonical `b-reference/teams-bot/architecture.md`, TOPICS, `b-reference/frontend-nyla/architecture.md` (Teams Bot UI row). (c-work: 2-build/2026-04-teamsbot-greenfield-ia-and-live-update.md)
## 2026-05-10
- 2026-05-10 | feat | gateway | CommCoach Fast Conversational Mode: `AgentConfig.excludeAllTools` flag fuer tool-freie Turns; generischer RAG-Session-Cache in `agentLoop` (TTL 120s / 5 Msgs); CommCoach nutzt beides fuer fluessige Coaching-Gespraeche
- 2026-05-10 | fix | gateway | RAG-Cache-Bug: doppelter `workflowId`-Parameter in `_getOrRefreshRag` behoben (Keyword-Kollision)
- 2026-05-10 | fix | gateway | CommCoach TTS: `_TTS_WORD_LIMIT` von 200 auf 80 Woerter reduziert; Sprachausgabe fasst laengere Antworten jetzt zusammen statt alles vorzulesen
- 2026-05-10 | feat | gateway | `AgentConfig.priority` (speed/quality/cost/balanced) an `agentLoop` durchgereicht; CommCoach conversational turns nutzen `PriorityEnum.SPEED` fuer schnellere Modellauswahl
- 2026-05-10 | fix | gateway | `interfaceDbManagement.getInterface` Singleton Race-Condition behoben: shared mutable instance ueberschrieb featureInstanceId zwischen async Requests; Agent konnte Files nicht finden
- 2026-05-10 | feat | frontend-nyla | TeamsBot UI aligned to greenfield IA: dashboard as KPI + module-activity + quick actions (no duplicate start form); MFA on live session SSE; assistant step adds join mode, MS account block, session context; modules header `Modul anlegen` dialog + `Meeting starten`; nav icons for assistant/modules; `FEATURE_REGISTRY` tab label Dashboard. (c-work: 2-build/2026-04-teamsbot-greenfield-ia-and-live-update.md)
- 2026-05-10 | feat, fix | gateway, frontend-nyla, teams-bot | TeamsBot: `startSession` binds `moduleId` with validation; DB migration M2 `TeamsbotMeetingModule.defaultMeetingLink` / `defaultBotName`; assistant prefill + searchable multi-row module list; modules edit saves defaults; session view polls while pending/joining even with SSE and syncs switcher row; browser bot chat compose marker + safe `AudioContext.close`. (c-work: 2-build/2026-04-teamsbot-greenfield-ia-and-live-update.md)
- 2026-05-10 | docs | wiki | Kanon: `b-reference/gateway/voice-google.md`; TOPICS + README Ordnerbaum; Querverweise in gateway/architecture, teams-bot/architecture, frontend-nyla/architecture, ai-agent.
@ -135,6 +166,13 @@ Skip: reine Refactors, Formatting, Lint, Dep-Bumps, Test-only, Wiki-Tippfehler.
- 2026-05-08 | chore | gateway | Rename env files: `env_{dev,int,prod,prod_forgejo}.env``env-gateway-{dev,int,prod,prod-forgejo}.env`. Updated GitHub/Forgejo workflows, deploy-gcp, .gitignore, .dockerignore, .gcloudignore, Dockerfile, scripts.
- 2026-05-08 | chore | gateway | Domain migration poweron-center.net → poweron.swiss: APP_API_URL, CORS origins, OAuth redirect URIs (MSFT, Google, ClickUp) in env-gateway-{int,prod,dev}. Updated billing email and doc URLs in stripeCheckout.py, datamodelTeamsbot.py.
## 2026-05-07
- 2026-05-07 | fix | gateway | sandboxExecutor: `open` durch In-Memory-VirtualFS ersetzt (statt geblockt); AI-generierter Code kann jetzt Dateien lesen/schreiben ohne echten Dateisystem-Zugriff
- 2026-05-07 | feat | gateway | RedmineTicketMirror + RedmineTicketDto: Feld `doneRatio` (% erledigt) ergaenzt; Sync-Mapping und DTO-Konvertierung aktualisiert
- 2026-05-07 | feat | gateway | Neues Agent-Tool `redmine.listRelations` fuer direkte Abfrage der Beziehungstabelle (Filter: issueId, relationType, pagination)
- 2026-05-07 | feat | gateway | `redmine.listTickets`: `offset`-Parameter ergaenzt fuer echte Pagination; Response liefert `offset` + `hasMore` statt `truncated`
## 2026-05-06
- 2026-05-06 | fix | frontend | UDB tab "Chatverläufe" renamed to "Dossiers" (UnifiedDataBar.tsx + i18n seed for xx/de/en/fr)
@ -142,6 +180,7 @@ Skip: reine Refactors, Formatting, Lint, Dep-Bumps, Test-only, Wiki-Tippfehler.
## 2026-05-05
- 2026-05-05 | fix | frontend-nyla | FormGeneratorTree: `window.confirm()` durch `useConfirm`-Modal ersetzt; Hover-Icons ueberlappen jetzt die Dateigroesse statt daneben zu stehen (spart Breite im File-Tree)
- 2026-05-05 | fix | gateway | 4 warning fixes: (1) mainServiceAgent skips getWorkflow for synthetic workflowIds with ":" prefix (commcoach billing keys); (2) ActionNodeExecutor + actionToolAdapter inject parentOperationId so ai.process progress nests correctly; (3) ChatService.interfaceDbComponent now receives featureInstanceId — root cause was missing scope causing getFile to miss featureInstance-scoped files during graph execution; (4) IMAGE_GENERATE sections use concise _buildImagePrompt instead of full 700KB userPrompt — eliminates DALL-E truncation
- 2026-05-05 | fix | gateway | _getOrCreateTempFolder rewritten to use ComponentObjects.getOwnFolderTree/createFolder instead of raw db.getRecordset bypass; added explicit logging for all code paths; _persistLargeDocument now logs updateFields before updateFile call
- 2026-05-05 | fix | frontend+gateway | AI-generated files not visible: (1) useCommcoach.ts sendMessage documentCreated handler used undefined `sessionId` instead of `session.id` causing ReferenceError silently swallowed by SSE parser catch-all — system note never shown; (2) SSE parser catch blocks narrowed to JSON.parse only so handler errors propagate; (3) _getOrCreateTempFolder re-implemented to find/create user Temp folder; (4) all file-update call sites consolidated into single updateFile call to avoid RBAC scope conflict
@ -333,30 +372,4 @@ Skip: reine Refactors, Formatting, Lint, Dep-Bumps, Test-only, Wiki-Tippfehler.
- 2026-04-25 | fix | frontend-nyla | DataPicker-Modal auf CSS-Variablen umgestellt; Hover-Safety-Net `dataPickerLeaf:hover *` haelt Type-Hints auf blauem Hintergrund lesbar (c-work: c-work/4-done/2026-04-feature-instance-ref-adapter-migration.md)
- 2026-04-25 | docs | wiki | Audit `2026-04-node-typization-audit.md` archiviert; Folge-Track-Doc `2026-04-feature-instance-ref-adapter-migration.md` direkt in `4-done/` als erledigt
- 2026-04-25 | docs | wiki | Changelog-Konvention im `_CHANGELOG.md` eingefuehrt; in `README.md` + `doc-sync.mdc` referenziert
- 2026-05-05 | fix | frontend-nyla | FormGeneratorTree: `window.confirm()` durch `useConfirm`-Modal ersetzt; Hover-Icons ueberlappen jetzt die Dateigroesse statt daneben zu stehen (spart Breite im File-Tree)
- 2026-05-07 | fix | gateway | sandboxExecutor: `open` durch In-Memory-VirtualFS ersetzt (statt geblockt); AI-generierter Code kann jetzt Dateien lesen/schreiben ohne echten Dateisystem-Zugriff
- 2026-05-07 | feat | gateway | RedmineTicketMirror + RedmineTicketDto: Feld `doneRatio` (% erledigt) ergaenzt; Sync-Mapping und DTO-Konvertierung aktualisiert
- 2026-05-07 | feat | gateway | Neues Agent-Tool `redmine.listRelations` fuer direkte Abfrage der Beziehungstabelle (Filter: issueId, relationType, pagination)
- 2026-05-07 | feat | gateway | `redmine.listTickets`: `offset`-Parameter ergaenzt fuer echte Pagination; Response liefert `offset` + `hasMore` statt `truncated`
- 2026-05-10 | feat | gateway | CommCoach Fast Conversational Mode: `AgentConfig.excludeAllTools` flag fuer tool-freie Turns; generischer RAG-Session-Cache in `agentLoop` (TTL 120s / 5 Msgs); CommCoach nutzt beides fuer fluessige Coaching-Gespraeche
- 2026-05-10 | fix | gateway | RAG-Cache-Bug: doppelter `workflowId`-Parameter in `_getOrRefreshRag` behoben (Keyword-Kollision)
- 2026-05-10 | fix | gateway | CommCoach TTS: `_TTS_WORD_LIMIT` von 200 auf 80 Woerter reduziert; Sprachausgabe fasst laengere Antworten jetzt zusammen statt alles vorzulesen
- 2026-05-10 | feat | gateway | `AgentConfig.priority` (speed/quality/cost/balanced) an `agentLoop` durchgereicht; CommCoach conversational turns nutzen `PriorityEnum.SPEED` fuer schnellere Modellauswahl
- 2026-05-10 | fix | gateway | `interfaceDbManagement.getInterface` Singleton Race-Condition behoben: shared mutable instance ueberschrieb featureInstanceId zwischen async Requests; Agent konnte Files nicht finden
- 2026-05-11 | feat | gateway | ToolDefinition.displayLabel: neues Feld fuer menschenlesbare Aktivitaetsbeschreibungen; Agent-Loop propagiert Label in TOOL_CALL Events; TeamsBot nutzt es fuer kontextuelle Fortschrittsmeldungen statt "Schritt N von M"
- 2026-05-11 | fix | gateway | TeamsBot Speaker-Attribution: retroaktive Zuordnung von "Unknown"-Eintraegen erfolgt jetzt bei jedem neuen Caption-Speaker, nicht nur beim ersten
- 2026-05-11 | fix | service-teams-browser-bot | Chat-Scraper Root Cause: Timestamp-Separatoren ("22:01") wurden als Chat-Nachrichten erkannt weil der innerText-Fallback auch bei unbekanntem Autor griff; Fallback jetzt nur wenn Autor identifiziert wurde
- 2026-05-11 | fix | gateway | mainServiceWeb progressLogUpdate: fehlende `if operationId:` Guards bei Zeilen 101/125 verursachten "Operation None not found" Warnung wenn Agent-Tool webSearch ohne operationId aufrief
- 2026-05-11 | fix | gateway | SPEECH_TEAMS Prompt: spezifische Beispiele entfernt, Eskalationsregeln generisch gehalten
- 2026-05-12 | fix | service-teams-browser-bot | Auth bot PiP loop fix: in full Teams web app, chat toggle navigates to Chat section → PiP → periodic scan reopens → PiP again (endless loop). Fix: added isAuthMode flag to ChatProcedure; in auth mode: skip chat panel toggle entirely, skip periodic reopen, skip panel-open preflight for sendChatMessage. Chat send uses direct input lookup in the meeting view. Replaced _returnToMeetingIfPip() approach (reactive) with prevention (don't toggle)
- 2026-05-12 | fix | service-teams-browser-bot | Auth bot meeting minimized (PiP): chat button click hit Teams sidebar "Chats" navigation instead of in-meeting toggle → meeting minimized, all PeerConnections closed, video/audio broke. Fix: isDangerousNavButton filter excludes sidebar nav, tab-items, submenu triggers; aria-pressed fallback for panel detection in full Teams web app; _openMoreMenu scoped to calling toolbar
- 2026-05-11 | fix | service-teams-browser-bot | Anonymous bot stuck after lobby admission: _waitForMeetingAdmission now tracks lobby-to-meeting transition, waits patiently when lobby vanishes (= admitted), and reloads page as recovery; isInMeeting expanded with light-meetings + German selectors
- 2026-05-18 | feat | gateway+frontend | UDB Resolve Endpoint: neuer POST /datasources/resolve-flags Bulk-Endpoint liefert effective-Werte (neutralize, scope, ragIndexEnabled) fuer beliebige Pfade auch ohne eigenen DB-Record; Frontend entfernt client-seitige Vererbungslogik (_findCoveringDs), nutzt ausschliesslich Backend-Daten via resolvedFlags Map
- 2026-05-19 | fix | gateway | RAG Inventory /mandate: ImportError UserMandate behoben (war in datamodelUam statt datamodelMembership); refactored zu getUserMandatesByMandate(); RBAC membership-Check hinzugefuegt (verhindert Zugriff auf fremde Mandate via X-Mandate-Id)
- 2026-05-19 | fix | gateway+frontend | RAG Inventory Mandate-Dropdown: neuer Endpunkt GET /api/rag/inventory/my-mandates liefert nur Mandate mit aktiver Mitgliedschaft; Frontend nutzt diesen statt /api/mandates/ (admin-only Route)
- 2026-05-19 | feat | gateway+frontend | RAG Inventory Feature-Daten: featureInstances-Array mit Datei-/Chunk-Zaehler, Status-Breakdown (indexed/pending/failed), FeatureDataSource-Details mit ragIndexEnabled-Flag, RAG-Aktiv-Indikator; Frontend zeigt Sync-Status-Banner, Datenquellen pro Instanz, RAG-Hinweis wenn inaktiv; leere Instanzen werden ausgefiltert
- 2026-05-19 | feat | gateway+frontend | Feature-Daten RAG-Sync: neuer Background-Job-Handler feature.bootstrap (subFeatureBootstrap.py) indexiert FeatureDataSource-Tabellendaten via FeatureDataProvider+requestIngestion; Handler-Registrierung in registerKnowledgeIngestionConsumer; neuer Endpoint POST /api/rag/inventory/reindex-feature/{workspaceInstanceId} mit FeatureAccess-Check; _buildFeatureInstanceInventory liefert runningJobs/lastSuccess/lastError; Frontend: Job-basierte Sync-Status-Banner + Reindex-Button fuer Feature-Instanzen (analog zu Connection-Sync)
- 2026-05-19 | fix | gateway | FeatureDataProvider: alle DB-Zugriffe (getActualColumns, browseTable, aggregateTable, queryTable, _resolveInstanceColumn) von deprecated db.connection.cursor() auf db.borrowCursor() migriert (Pool-API-Kompatibilitaet nach Pooling-Refactoring)
- 2026-05-19 | fix | gateway | Trustee Budget-Vergleich: _exportAccountingData liefert neu eine accountSummary (1 Zeile/Konto mit closingBalance + Q1-Q4) statt 5590 rohe Balance-Records (26/Konto); AI-Prompt praezisiert Datenquelle; reduziert Payload von 355KB auf 25KB und verhindert Faktor-100-Abweichungen durch versehentliches Summieren
- 2026-05-23 | feat | gateway+frontend | DB Migration Progress: Export und Import laufen per-DB sequentiell mit Echtzeit-Fortschrittslog (Timestamp, DB-Name, Tabellen-/Datensatzzahl, Fehler); neuer export-single Endpoint; Import via prepare-import + import-single (server-seitiger Payload-Cache mit Token); Export-Dateiname enthaelt Instanzlabel + full/partial (z.B. db_backup_main_full_2026-05-23T06-20-54.json)

Binary file not shown.