# 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` und ein `Set`. 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|", "feat|||", ...] } Response: { "nodesByParent": { "__root__": [...], "conn|": [...], ... } } ``` `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\|` | UserConnection-Root | | `svc\|\|` | Service unter Connection (sharepoint, outlook, ...) | | `ds\|\|\|` | Folder oder File innerhalb eines Services | | `mgrp\|` | Mandanten-Gruppen-Kopfknoten | | `feat\|\|\|` | Feature-Instanz (Workspace-Wildcard `*`) | | `fdstbl\|\|` | Feature-Datentabelle | | `fdsrec\|\|\|` | (reserviert, derzeit nicht emittiert) | ### Builder `platform-core/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` - `expandedKeys: Set` - `pendingToggles: Set` (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: - `platform-core/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: `platform-core/modules/serviceCenter/services/serviceKnowledge/_buildTree.py`, `_inheritFlags.py`, `platform-core/modules/features/workspace/routeFeatureWorkspace.py`, `platform-core/modules/routes/routeDataSources.py`, `platform-core/modules/datamodels/datamodelFeatureDataSource.py` - Frontend: `ui-nyla/src/components/UnifiedDataBar/SourcesTab.tsx` - Tests: `platform-core/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`).