wiki/b-reference/platform/unified-data-bar.md

15 KiB
Raw Permalink Blame History

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

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:

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, "<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:

  1. buildNodeForKey(nodeKey, ...) parsed den Key in die zustaendige UdbNode-Subklasse.
  2. node.supportsFlag(flag) → 400 wenn nicht.
  3. node.canEdit(context, rootIf) → 403 wenn nicht.
  4. node.setFlag(flag, value, rootIf) persistiert + cascade-reset, liefert die Liste der zurueckgesetzten Descendant-Ids.
  5. _computeEffectiveAfterWrite(...) berechnet den frischen effective Wert (inkl. mixed).
  6. Audit-Log via audit_logger.logEvent(action="udb_flag_changed", ...).

value=null setzt den eigenen Wert auf null (vererbe wieder) ohne Cascade-Reset und ohne Index-Purge.

Key-Format

Pattern Beschreibung
personalRoot Top-Level-Container der eigenen Connections
mgrp|<mandateId> Mandanten-Kopfknoten
conn|<connId> UserConnection-Root
svc|<connId>|<service> Service unter einer Connection (sharepoint, outlook, drive, ...)
ds|<connId>|<sourceType>|<path> Folder oder File innerhalb eines Services
feat|<mandateId>|<featureCode>|<fiId> Feature-Instanz (Workspace-Wildcard *)
fdstbl|<fiId>|<tableName> Feature-Datentabelle
fdsfld|<fiId>|<tableName>|<fieldName> Virtuelles Feld unter einer Table

buildNodeForKey(...) ist die einzige Stelle, die Keys decodiert; alle anderen Stellen erhalten bereits getypte UdbNode-Instanzen.

Frontend-Vertrag

ui-nyla/src/components/UnifiedDataBar/UdbSourcesProvider.tsx:

  • Einziger Cache: Map<key, UdbBackendNode> plus expanded-Set.
  • loadChildren(parent)POST /api/udb/tree/children.
  • patchNeutralize / patchRagIndexPOST /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.