From 8ee13275225d9efa4e48012c5fbb09f5eb0c0a12 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Wed, 27 May 2026 16:49:03 +0200 Subject: [PATCH] fixes udb --- TOPICS.md | 6 +- b-reference/platform/neutralization.md | 24 +- b-reference/platform/rbac.md | 18 +- b-reference/platform/unified-data-bar.md | 243 ++++++++++++++++++ .../2026-05-udb-polymorphic-refactor.md | 141 ++++++++++ .../2026-05-udb-toggle-spec-recovery.md | 85 ++++++ .../2026-05-udb-generic-tree-refactor.md | 14 + c-work/_CHANGELOG.md | 65 +++-- d-guides/deployment/poweron-sec.kdbx | Bin 28958 -> 29310 bytes 9 files changed, 556 insertions(+), 40 deletions(-) create mode 100644 b-reference/platform/unified-data-bar.md create mode 100644 c-work/3-validate/2026-05-udb-polymorphic-refactor.md create mode 100644 c-work/3-validate/2026-05-udb-toggle-spec-recovery.md diff --git a/TOPICS.md b/TOPICS.md index 40074b8..d5b6eef 100644 --- a/TOPICS.md +++ b/TOPICS.md @@ -1,5 +1,5 @@ - + # 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 | diff --git a/b-reference/platform/neutralization.md b/b-reference/platform/neutralization.md index c7147fd..e386b2e 100644 --- a/b-reference/platform/neutralization.md +++ b/b-reference/platform/neutralization.md @@ -1,6 +1,6 @@ - - + + # 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..]` 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, "", "", ...]}`. -- **Response:** `{nodesByParent: {"__root__": [...], "": [...], ...}}` — 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, "", ...]}`. 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 diff --git a/b-reference/platform/rbac.md b/b-reference/platform/rbac.md index 3122db6..ba13dc5 100644 --- a/b-reference/platform/rbac.md +++ b/b-reference/platform/rbac.md @@ -1,6 +1,6 @@ - - + + # 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) diff --git a/b-reference/platform/unified-data-bar.md b/b-reference/platform/unified-data-bar.md new file mode 100644 index 0000000..3eee5fc --- /dev/null +++ b/b-reference/platform/unified-data-bar.md @@ -0,0 +1,243 @@ + + + + +# 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, "", "", ...] } +Response: { "nodesByParent": { "__root__": [...], "": [...], ... } } +``` + +`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": } +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\|` | Mandanten-Kopfknoten | +| `conn\|` | UserConnection-Root | +| `svc\|\|` | Service unter einer Connection (sharepoint, outlook, drive, ...) | +| `ds\|\|\|` | Folder oder File innerhalb eines Services | +| `feat\|\|\|` | Feature-Instanz (Workspace-Wildcard `*`) | +| `fdstbl\|\|` | Feature-Datentabelle | +| `fdsfld\|\|\|` | 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` 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`. diff --git a/c-work/3-validate/2026-05-udb-polymorphic-refactor.md b/c-work/3-validate/2026-05-udb-polymorphic-refactor.md new file mode 100644 index 0000000..4876fc5 --- /dev/null +++ b/c-work/3-validate/2026-05-udb-polymorphic-refactor.md @@ -0,0 +1,141 @@ + + + + + +# 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) diff --git a/c-work/3-validate/2026-05-udb-toggle-spec-recovery.md b/c-work/3-validate/2026-05-udb-toggle-spec-recovery.md new file mode 100644 index 0000000..f7bb530 --- /dev/null +++ b/c-work/3-validate/2026-05-udb-toggle-spec-recovery.md @@ -0,0 +1,85 @@ + + + + + +# 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. diff --git a/c-work/4-done/2026-05-udb-generic-tree-refactor.md b/c-work/4-done/2026-05-udb-generic-tree-refactor.md index b705480..a2a8d9a 100644 --- a/c-work/4-done/2026-05-udb-generic-tree-refactor.md +++ b/c-work/4-done/2026-05-udb-generic-tree-refactor.md @@ -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`). diff --git a/c-work/_CHANGELOG.md b/c-work/_CHANGELOG.md index c794b09..73111cb 100644 --- a/c-work/_CHANGELOG.md +++ b/c-work/_CHANGELOG.md @@ -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:]` (`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 `