wiki/c-work/4-done/2026-05-udb-generic-tree-refactor.md
2026-05-27 16:49:03 +02:00

172 lines
10 KiB
Markdown

<!-- status: done -->
<!-- started: 2026-05-18 -->
<!-- completed: 2026-05-18 -->
<!-- component: gateway | frontend-nyla -->
<!-- lastReviewed: 2026-05-18 -->
<!-- verifiedAgainst: gateway/modules/serviceCenter/services/serviceKnowledge/_buildTree.py | gateway/modules/serviceCenter/services/serviceKnowledge/_inheritFlags.py | gateway/modules/features/workspace/routeFeatureWorkspace.py | gateway/modules/routes/routeDataSources.py | gateway/modules/datamodels/datamodelFeatureDataSource.py | frontend_nyla/src/components/UnifiedDataBar/SourcesTab.tsx -->
# UDB Generic Tree Refactor (Backend authoritativ, FE pure Renderer)
## Kontext
Die `SourcesTab.tsx` des Unified Data Bars trug ein wachsendes Konsistenzproblem: Toggles auf Parent-Nodes hatten unter bestimmten Bedingungen keinen UI-Effekt. Wiederholte Bugfix-Iterationen am gleichen Datum (siehe `2026-05-udb-cascade-inherit.md`) haben drei Symptome verschoben statt geheilt, weil im Frontend parallele Logik (`_resolveFdsFlags`, optimistic Updates, `_findCoveringDs`) den vom Backend gelieferten Werten widerspricht.
User-Direktive: Das Backend ist die einzige Quelle der Wahrheit fuer die effektiven Flag-Werte aller sichtbaren Tree-Nodes. Das Frontend rendert nur. Keine Vererbung im Frontend. Keine optimistischen Updates.
## Ziele
1. Ein einziger Endpoint liefert auf Anfrage Children-Listen pro Parent-Key, inklusive aller drei effektiven Flag-Werte (`neutralize`, `scope`, `ragIndexEnabled`) als `boolean | "mixed"` bzw. `string | "mixed"`.
2. Das Frontend halt nur einen `Map<parentKey, TreeNode[]>` und ein `Set<expandedKeys>`. Re-Render = Re-Fetch.
3. Toggle-Flow strikt: PATCH -> Refetch -> Render. Pro Klick wird der entsprechende Flag-Button als Spinner gezeigt, bis die neuen Daten gerendert sind.
4. Ein einziges, von allen anderen Symbolen klar unterscheidbares `mixed`-Symbol gilt fuer alle drei Flags.
5. RAG-Indexierung ist auf der gleichen Stufenmechanik fuer FDS verfuegbar wie fuer Personal-DataSources.
## Architektur
### Tree-Endpoint
```
POST /api/workspace/{instanceId}/tree/children
Body: { "parents": [null, "conn|<id>", "feat|<mid>|<code>|<fiId>", ...] }
Response: { "nodesByParent": { "__root__": [...], "conn|<id>": [...], ... } }
```
`null` markiert die Top-Level-Ebene. Pro Parent-Key kommt eine Liste von `TreeNode`-Dicts zurueck.
`TreeNode` ist die einzige Schema-Struktur fuer alle Kinds:
```
key, kind, parentKey, label, icon, hasChildren,
dataSourceId, modelType,
effectiveNeutralize, effectiveScope, effectiveRagIndexEnabled,
supportsRag, canBeAdded,
+ kind-spezifische Carrier (authority, connectionId, service, sourceType, path,
featureInstanceId, featureCode, mandateId, tableName, objectKey)
```
Mit `kind in {connection, service, folder, file, mandateGroup, featureNode, fdsTable, fdsRecord}`. Der `fdsRecord`-Kind ist im Schema vorgesehen, aber im aktuellen Builder nicht aktiviert (Records werden bewusst nicht expandierbar gerendert, siehe "Bewusste Reduzierung" unten).
### Key-Format
Stable, parsebar, kollisionsfrei. Pipe-Separator:
| Pattern | Bedeutung |
|---|---|
| `conn\|<connId>` | UserConnection-Root |
| `svc\|<connId>\|<service>` | Service unter Connection (sharepoint, outlook, ...) |
| `ds\|<connId>\|<sourceType>\|<path>` | Folder oder File innerhalb eines Services |
| `mgrp\|<mandateId>` | Mandanten-Gruppen-Kopfknoten |
| `feat\|<mandateId>\|<featureCode>\|<fiId>` | Feature-Instanz (Workspace-Wildcard `*`) |
| `fdstbl\|<fiId>\|<tableName>` | Feature-Datentabelle |
| `fdsrec\|<fiId>\|<tableName>\|<recordId>` | (reserviert, derzeit nicht emittiert) |
### Builder
`gateway/modules/serviceCenter/services/serviceKnowledge/_buildTree.py` orchestriert pro Request:
- Vor-Laden aller DataSource-Records des Users (`recordFilter={"userId": userId}`) und aller FeatureDataSource-Records des Workspaces (`recordFilter={"workspaceInstanceId": instanceId}`) einmal.
- Dispatch pro Parent-Kind:
- `null` -> aktive Connections + zugaengliche Mandanten-Gruppen
- `conn|*` -> verfuegbare Services (`provider.getAvailableServices()`)
- `svc|*` / `ds|*` -> `adapter.browse(path)` via ConnectorResolver
- `mgrp|*` -> per-Mandate Feature-Instanzen mit `featureCode in featuresWithDataObjects`
- `feat|*` -> `catalog.getDataObjects()` gefiltert via RBAC
- Pro Node werden die drei `effective*`-Werte ueber `resolveEffectiveForPath` / `resolveEffectiveForFds` (mode=`aggregate`) berechnet. Diese Helpers funktionieren auch fuer Coordinates ohne eigenen DB-Record (virtueller Datensatz mit `null`-Flags).
- Sourcetype-Mapping (`sharepoint -> sharepointFolder`, `onedrive -> onedriveFolder`, ...) lebt nun ausschliesslich im Builder, nicht mehr im Frontend.
### Frontend (`SourcesTab.tsx`)
`~530` Zeilen, ersetzt die alte `~2500`-Zeilen-Version komplett.
```mermaid
sequenceDiagram
participant User
participant FE as SourcesTab
participant BE as Backend
User->>FE: Klick auf Flag-Button (Knoten X, Flag F)
FE->>FE: pendingToggles.add(X|F) -> Spinner sichtbar
alt Knoten hat keinen DB-Record
FE->>BE: POST /datasources oder /feature-datasources
BE-->>FE: { id }
end
FE->>BE: PATCH /api/datasources/{id}/{F} Body: {F: !effective}
BE-->>FE: 200 OK
FE->>BE: POST /tree/children Body: {parents: [null, ...expandedKeys]}
BE-->>FE: nodesByParent (alle effective* aktualisiert)
FE->>FE: setChildrenByParent + pendingToggles.delete -> Re-Render
```
State (im Component):
- `childrenByParent: Map<string, TreeNode[]>`
- `expandedKeys: Set<string>`
- `pendingToggles: Set<string>` (Key-Form `${nodeKey}|${flag}`)
Re-fetch wird ueber einen Signatur-Effect getriggert, sobald `expandedKeys` aendert.
Generischer `_FlagButton` rendert:
- Spinner waehrend `pending`
- Mixed-Symbol (`U+25E9`, "square with diagonal lines") wenn `effective* === "mixed"` (gilt einheitlich fuer alle 3 Flags)
- Sonst Flag-spezifisches Icon (Lock fuer neutralize, Brain fuer RAG, Personen/Building/Globus fuer Scope)
### FDS-RAG-Inheritance
- `_INHERITABLE_FDS_FLAGS` enthaelt nun `ragIndexEnabled`.
- `FeatureDataSource.ragIndexEnabled: Optional[bool]` Field (Default `None` = inherit; Auto-Migration via `ALTER TABLE ADD COLUMN`).
- `PATCH /api/datasources/{id}/rag-index` akzeptiert sowohl DataSource- als auch FeatureDataSource-IDs (via `_findSourceRecord`). Bootstrap-Job und Chunk-Purge bleiben DataSource-only (FDS-RAG ist Feature-Pipeline-getrieben, Flag genuegt).
- `GET /api/workspace/{instanceId}/feature-datasources` liefert `effectiveRagIndexEnabled`.
- `resolveEffectiveForFds` liefert den dritten Wert konsistent mit dem DS-Pendant.
## Bewusste Reduzierung
FDS-Records (z.B. "Mandant Mueller" als einzelner Datensatz innerhalb der `Mandanten`-Tabelle) sind im neuen Tree nicht expandierbar (`hasChildren: False` fuer `fdsTable`). Begruendung: User-Entscheidung 2026-05-18, Tabellen-Ebene reicht. Das `FeatureDataSource.recordFilter`-Feld bleibt im Datenmodell und in `POST /feature-datasources` fuer API-Kompatibilitaet erhalten, wird aber von der UI nicht mehr gesetzt.
## Entfernter Code (Backend)
Vollstaendig geloescht (keine Aufrufer mehr im Frontend, kein deprecated-Zustand):
- `POST /api/workspace/{instanceId}/datasources/resolve-flags` und Models `_ResolveFlagsRequest`, `_ResolveFlagItem`, `_ResolveFlagFdsItem`
- `GET /api/workspace/{instanceId}/connections`
- `GET /api/workspace/{instanceId}/connections/{connectionId}/services`
- `GET /api/workspace/{instanceId}/connections/{connectionId}/browse`
- `GET /api/workspace/{instanceId}/feature-connections`
- `GET /api/workspace/{instanceId}/feature-connections/{fiId}/tables`
- `GET /api/workspace/{instanceId}/feature-connections/{fiId}/parent-objects/{tableName}`
Erhalten (haben weitere Konsumenten ausserhalb UDB):
- `GET /api/workspace/{instanceId}/datasources` -> useWorkspace.ts, WorkspacePage.tsx, Commcoach*, GraphicalEditorPage.tsx
- `GET /api/workspace/{instanceId}/feature-datasources` -> selbe Konsumenten
- `POST .../datasources` und `POST .../feature-datasources` -> neuer SourcesTab nutzt sie fuer `_ensureRecord`
## Entfernter Code (Frontend)
- Komplettes altes `SourcesTab.tsx` (~2500 Zeilen): `_resolveFdsFlags`, `resolvedFdsFlags`, `_findCoveringDs`, `_readFdsFlags`, alle optimistic `setDataSources(prev.map(...))` und `setFeatureDataSources(prev.map(...))`, prop-drilling `inheritedScope`/`inheritedNeutralize`, mehrere Sub-Komponenten (`_FeatureTableRow`, `_FeatureActionContext`, etc.)
- Totes CSS-Modul `SourcesTab.module.css`
## Tests
Neu:
- `gateway/tests/unit/services/test_buildTree.py` (10 Tests): Key-Encoding/Decoding, `_effectiveTripletDs/Fds` Defaults+Inheritance, Record-Lookup (DS path-normalisation; FDS record-filter equality), `getChildrenForParents`-Smoke (unknown parent -> `[]`, leeres Top-Level).
- 5 neue Tests in `test_inheritFlags.py::TestResolveEffectiveForFds`: RAG inherits when only neutralize overridden; RAG aggregate-mixed; `_INHERITABLE_FDS_FLAGS` contains all 3 keys.
- `TestCascadeResetFdsRag::test_cascade_resets_rag_on_descendants`.
Resultat: `tests/unit/services/test_inheritFlags.py` + `test_buildTree.py` = 82/82 gruen.
Frontend: `npx tsc --noEmit --skipLibCheck` clean.
## Verifizierte Datei-Pfade
- Backend: `gateway/modules/serviceCenter/services/serviceKnowledge/_buildTree.py`, `_inheritFlags.py`, `gateway/modules/features/workspace/routeFeatureWorkspace.py`, `gateway/modules/routes/routeDataSources.py`, `gateway/modules/datamodels/datamodelFeatureDataSource.py`
- Frontend: `frontend_nyla/src/components/UnifiedDataBar/SourcesTab.tsx`
- Tests: `gateway/tests/unit/services/test_buildTree.py`, `test_inheritFlags.py`
## Nachtrag 2026-05-26: Spec Recovery
Im Code waren nach dem Refactor sechs Aggregations-Routinen (`_aggregatePersonalRoot`,
`_aggregateConnection`, `_aggregateMandateGroup`, `_resolveAttrsForKey`,
`getAttributesForKeys`) und ein paralleler Endpoint `POST /tree/attributes`
entstanden, die der oben dokumentierten Single-Pipeline-Architektur widersprachen.
Zusaetzlich fehlte bei virtuellen Coordinates (Records ohne DB-Eintrag) die
Subtree-Aggregation, sodass `'mixed'` nie zurueckgegeben werden konnte.
Das Frontend nutzte `refreshAttributes` und `refreshAfterAction` als optionale
Pfade statt des einen Refetch-All-Expanded-Pfads.
Diese Abweichungen wurden am 2026-05-26 zurueckgefuehrt
(siehe `c-work/3-validate/2026-05-udb-toggle-spec-recovery.md`).