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

241 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- status: canonical -->
<!-- lastReviewed: 2026-06-03 -->
<!-- verifiedAgainst: platform-core/modules/serviceCenter/services/serviceKnowledge/udbNodes.py | _buildTree.py | _inheritFlags.py | platform-core/modules/routes/routeUdb.py | platform-core/modules/datamodels/datamodelFeatureDataSource.py | ui-nyla/src/components/UnifiedDataBar/UdbSourcesProvider.tsx -->
# Unified Data Bar (UDB)
Die UDB ist die zentrale Komponente zur Anzeige und Steuerung **aller** Datenquellen, die ein User im System sieht. Sie ist **feature-unabhaengig**: sie kann in jeder Feature-Instanz eingebunden werden, ist aber von keinem konkreten Feature abhaengig. Auch ein System-Tab ohne Feature-Kontext koennte die UDB nutzen.
Dieses Dokument ist die kanonische Quelle der Wahrheit fuer:
1. das Domain-Modell (welche Node-Typen, wer besitzt was)
2. die Flag-Mechanik (`neutralize` / `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, "<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` / `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`.