172 lines
10 KiB
Markdown
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`).
|