# 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` / `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/platform-core/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 | 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) | Owner-of-record (`rec.userId == user`) | | **FdsRecord** | `FdsWorkspaceNode`, `FdsTableNode`, `FdsRowNode` | `FeatureDataSource` (`featureInstanceId`-scoped) | ja (3-wertig) | ja (3-wertig) | Feature-Admin (`roleLabel.endswith('-admin')` auf der `featureInstanceId`) | | **FdsField** | `FdsFieldNode` | virtueller Child unter Table, persistiert in `FeatureDataSource.neutralizeFields` | ja (zweiwertig: enthalten in `neutralizeFields` oder nicht) | nein | wie FdsRecord | **Wichtig — Ownership-Trennung:** - `DataSource` ist **user-privat** (`userId`-scoped). Personal Sources sind ausschliesslich fuer den Owner sichtbar (kein Scope-Sharing). Die DB-Spalte `scope` ist deprecated (2026-06, Datenschutz) und wird nicht mehr gelesen oder geschrieben. Scope existiert nur noch bei Files (Folder-Files: eigene + geteilte Daten). - `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 Zwei 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`) 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 zwei 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` (kein `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). ## 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 File/Folder); fremde Attribute read-only | | Personal Sources | Nur eigene Connections + Sources (kein Scope-Sharing; Datenschutz, 2026-06) | | 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. ## 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, 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"`. `FdsFieldNode` traegt nur `effectiveNeutralize`. `effectiveScope` wird aus Kompatibilitaet immer als `"personal"` serialisiert, aber vom Frontend ignoriert. ### `POST /api/udb/node/{nodeKey}/flag/{flag}` ``` Path: nodeKey ∈ Tree-Keys (URL-encoded; '|' bleibt nach decodieren erhalten) flag ∈ {neutralize, 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`. - `patchNeutralize` / `patchRagIndex` → `POST /api/udb/node/{key}/flag/{flag}` mit `{value}`-Body. (`patchScope` ist seit 2026-06 ein No-op; Scope existiert nur noch bei Files.) - 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 Flags (`◩`, U+25E9). - Klick auf das mixed-Symbol → setzt explizit `false` (gilt fuer alle 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. - **Kein Scope auf DataSource oder FDS.** Scope wurde 2026-06 aus Datenschutzgruenden von DataSource entfernt (personal sources duerfen nicht gescoped werden). FDS hatte nie Scope. Scope existiert nur noch bei Files (folder-files: eigene + geteilte Daten). - **Audit ist Pflicht.** Jede `setFlag`-Ausfuehrung schreibt einen `udb_flag_changed`-Eintrag mit `nodeKey`, `flag`, `value`, `resetDescendants`, `nodeKind`.