241 lines
15 KiB
Markdown
241 lines
15 KiB
Markdown
<!-- 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`.
|