15 KiB
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:
- das Domain-Modell (welche Node-Typen, wer besitzt was)
- die Flag-Mechanik (
neutralize/ragIndexEnabled, Vererbung, mixed-Aggregation, Cascade-Reset) - die RBAC-Regeln fuer Flag-Aenderungen
- 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 zentralenif 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
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:
DataSourceist user-privat (userId-scoped). Personal Sources sind ausschliesslich fuer den Owner sichtbar (kein Scope-Sharing). Die DB-Spaltescopeist 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 keinenuserId, keinworkspaceInstanceId, keinscope-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, gleicheconnectionId+sourceTypefuer DS, gleichefeatureInstanceId+tableNamefuer 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:
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 dergetLogicalChildren(...). 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 eigeneneutralizeFields-Liste wenn ein expliziterneutralize-Wert gesetzt wird (per-column Overrides werden durch den Tabellen-Wert obsolet).FdsWorkspaceNode._onSetFlag: wipe-tneutralizeFieldsauf 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:
fieldName in tableRec.neutralizeFields→ expliziter Override →effectiveNeutralize=True.- sonst → das Feld erbt vom effektiven
neutralizeseiner 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.idwenn einDataSource-Record existiert. Fuer virtuelle Nodes (Browse-Folder ohne Record) prueftcanEditstattdessen, ob dieUserConnectiondem User gehoert (_isConnectionOwner);setFlagerstellt dann automatisch einen DataSource-Stub-Record. Geteilte Sources (anderer User) bleiben read-only._FdsFamilyNode(FdsWorkspace / FdsTable / FdsRow),FdsFieldNode:_isFeatureAdmin(rootIf, userId, featureInstanceId)— verlangt eineFeatureAccessRoleauf der Instanz, derenRole.roleLabelmit-adminendet (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, "<key1>", "<key2>", ...] }
Response: { "nodesByParent": { "__root__": [...], "<key1>": [...], ... } }
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": <bool | null> }
Response: { "nodeKey", "flag", "value", "effective", "resetDescendantIds" }
Ablauf:
buildNodeForKey(nodeKey, ...)parsed den Key in die zustaendigeUdbNode-Subklasse.node.supportsFlag(flag)→ 400 wenn nicht.node.canEdit(context, rootIf)→ 403 wenn nicht.node.setFlag(flag, value, rootIf)persistiert + cascade-reset, liefert die Liste der zurueckgesetzten Descendant-Ids._computeEffectiveAfterWrite(...)berechnet den frischen effective Wert (inkl. mixed).- 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.patchNeutralize/patchRagIndex→POST /api/udb/node/{key}/flag/{flag}mit{value}-Body. (patchScopeist 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 aufnull/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/childrenfuer 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 einenudb_flag_changed-Eintrag mitnodeKey,flag,value,resetDescendants,nodeKind.