wiki/c-work/4-done/2026-05-udb-generic-tree-refactor.md
2026-05-19 16:47:48 +02:00

9.4 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

  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.

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