10 KiB
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
- Ein einziger Endpoint liefert auf Anfrage Children-Listen pro Parent-Key, inklusive aller drei effektiven Flag-Werte (
neutralize,scope,ragIndexEnabled) alsboolean | "mixed"bzw.string | "mixed". - Das Frontend halt nur einen
Map<parentKey, TreeNode[]>und einSet<expandedKeys>. Re-Render = Re-Fetch. - Toggle-Flow strikt: PATCH -> Refetch -> Render. Pro Klick wird der entsprechende Flag-Button als Spinner gezeigt, bis die neuen Daten gerendert sind.
- Ein einziges, von allen anderen Symbolen klar unterscheidbares
mixed-Symbol gilt fuer alle drei Flags. - 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
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-Gruppenconn|*-> verfuegbare Services (provider.getAvailableServices())svc|*/ds|*->adapter.browse(path)via ConnectorResolvermgrp|*-> per-Mandate Feature-Instanzen mitfeatureCode in featuresWithDataObjectsfeat|*->catalog.getDataObjects()gefiltert via RBAC
- Pro Node werden die drei
effective*-Werte ueberresolveEffectiveForPath/resolveEffectiveForFds(mode=aggregate) berechnet. Diese Helpers funktionieren auch fuer Coordinates ohne eigenen DB-Record (virtueller Datensatz mitnull-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.
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") wenneffective* === "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_FLAGSenthaelt nunragIndexEnabled.FeatureDataSource.ragIndexEnabled: Optional[bool]Field (DefaultNone= inherit; Auto-Migration viaALTER TABLE ADD COLUMN).PATCH /api/datasources/{id}/rag-indexakzeptiert 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-datasourceslieferteffectiveRagIndexEnabled.resolveEffectiveForFdsliefert 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-flagsund Models_ResolveFlagsRequest,_ResolveFlagItem,_ResolveFlagFdsItemGET /api/workspace/{instanceId}/connectionsGET /api/workspace/{instanceId}/connections/{connectionId}/servicesGET /api/workspace/{instanceId}/connections/{connectionId}/browseGET /api/workspace/{instanceId}/feature-connectionsGET /api/workspace/{instanceId}/feature-connections/{fiId}/tablesGET /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.tsxGET /api/workspace/{instanceId}/feature-datasources-> selbe KonsumentenPOST .../datasourcesundPOST .../feature-datasources-> neuer SourcesTab nutzt sie fuer_ensureRecord
Entfernter Code (Frontend)
- Komplettes altes
SourcesTab.tsx(~2500 Zeilen):_resolveFdsFlags,resolvedFdsFlags,_findCoveringDs,_readFdsFlags, alle optimisticsetDataSources(prev.map(...))undsetFeatureDataSources(prev.map(...)), prop-drillinginheritedScope/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/FdsDefaults+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_FLAGScontains 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).