wiki/c-work/3-validate/2026-05-udb-sources-recovery.md
2026-05-19 16:47:48 +02:00

387 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- status: validate -->
<!-- started: 2026-05-18 -->
<!-- component: frontend-nyla | gateway -->
# UDB Sources Recovery: einheitliche Tree-Mechanik via FormGeneratorTree
## Status (2026-05-18, abends)
| Phase | Status | Tests |
|-------|--------|-------|
| 1. Backend Layout-Synthese (`_buildTree.py`) | DONE | `test_buildTree.py` 16/16, `test_inheritFlags.py` 72/72 -> Service-Suite **88/88 gruen** |
| 2. FormGeneratorTree generisch erweitern | DONE | `FormGeneratorTree.test.tsx` 50/50 -> +RAG / `displayOrder` / `refreshAfterAction` / `defaultExpanded` |
| 3. SourcesTab + UdbSourcesProvider neu | DONE | `UdbSourcesProvider.test.ts` 16/16, **Tree+Provider total 66/66 gruen** |
| 4. Folge-UX: flat layout / default-expand / icon-order / settings-on-root / on-off icons | DONE | Tests inklusive (siehe oben) |
| 5. Validierung (User-Smoke + Wiki-Update) | OPEN | manueller Test im Browser ausstehend |
**Folge-Iteration (selbe Sitzung, nach Phase 1-3 User-Feedback)**:
- F1 -- Visual cascade fuer `neutralize` + `ragIndexEnabled`: vorher *gleiches Icon* fuer on/off (nur Opacity). Backend-Cascade war korrekt; verwirrend war nur das Icon. Distinct emojis: `closed lock` vs `open lock`, `brain` vs `thought-bubble`. (Cascade-Logik im Backend identisch zu `scope`, von `cascadeResetDescendants(rec, flag)` parametrisch. Test-Suite belegt das in `TestCascadeReset` mit `flag="neutralize"`.)
- F2 -- Top-Level-Layout flach: `srcRoot` und `mandateRoot` entfernt. Top-Level emittiert nun direkt `[personalRoot, mgrp|m1, mgrp|m2, ...]`. Mandate-Groups sind keine Children eines Wrappers mehr. Tree-Header kommt aus dem `title`-Prop von `FormGeneratorTree`.
- F3 -- `TreeNode.defaultExpanded` als generisches Tree-Feature: einmal pro Node-Id auto-expand bei initialem Load (one-shot via `autoExpandedRef`). Backend markiert `personalRoot` und alle Mandate-Groups mit `defaultExpanded=True`. Manuelles Collapse ueberschreibt nachher; Refetches re-emitten dieselbe Id, aber die Map-Ref blockt Re-Trigger.
- F4 -- Icon-Reihenfolge in `FormGeneratorTree` (rechts-buendig, von rechts nach links): neutralize, scope, sendToChat, rag, settings. Settings (`extraActions`) jetzt linksbuendig. Settings-Icon wird im `UdbSourcesProvider` nur noch fuer `kind in {connection, featureNode}` (= Daten-Quellen-Root) angehaengt -- aber dort *immer*, auch ohne `dataSourceId`. Klick triggert `_ensureRecord` -> `onOpenSettings(dsId, label)`.
- F5 -- `SourcesTab` setzt `title={t('Datenquellen')}` als Section-Header.
**G-Iteration (Smoke-Test-Feedback, gleiche Sitzung)**:
- G1 -- 139 TS-Lint-Fehler in `FormGeneratorTree.test.tsx`: Wurzel war fehlende tsconfig-Reference auf `tsconfig.test.json` (LSP fiel fuer `*.test.tsx` auf den App-Config zurueck, der Tests ausdruecklich `exclude`d). Fix: `tsconfig.json.references` um `./tsconfig.test.json` erweitert + im Testfile expliziter `import React`, `import 'vitest/globals'`-aequivalent (`@testing-library/jest-dom/vitest`), `afterEach` aus `vitest`. 139 -> 0; Tests 50/50 weiterhin gruen.
- G2 -- Off-State-Differenzierung: offenes Schloss + RAG-OFF (Brain) bekommen `filter: grayscale(1); opacity: .45` als inline-style. ON-Zustand bleibt voll farbig. Eindeutige Unterscheidung auf einen Blick.
- G3 -- Sub-Element-Expansion:
- **Bug**: `_browseChildren` setzte `parentKey` auf die synthesisierte ds-Koordinate (`ds|conn|sourceType|/`), aber der Anrufer hat den `svc|...`-Key erwartet. Folge: zurueckgegebene Folders/Files hatten einen `parentId`, der nicht im Tree existiert; sie wurden stillschweigend verworfen. Fix: `_browseChildren` akzeptiert jetzt `parentKey` und nutzt ihn fuer Childs.
- **Feature-Felder**: `fdsTable` hat jetzt `hasChildren: True` wenn das DataObject-Meta `fields: List[str]` deklariert. Neuer Knoten-Typ `fdsField` (kind), neuer Dispatcher-Zweig `fdstbl|fi|table` -> `_featureTableFields`. Effektiver Neutralize-Wert pro Feld = `parent.neutralize OR field IN parent.neutralizeFields`. Toggle wird im `UdbSourcesProvider` per Special-Case auf `PATCH /api/datasources/{id}/neutralize-fields` mit der mutierten Liste umgesetzt; `neutralizeFields` ist im fdsTable-Payload mit drin (kein Extra-GET). Scope und RAG auf Feld-Ebene sind bewusst nicht editierbar.
- G4 -- FilesTab: synthetischer "/"-Root pro Ownership. Provider gibt fuer `parentId=null` einen `__filesRoot:<ownership>`-Knoten zurueck (`defaultExpanded=true`); reale Top-Level-Items werden Children dieses Roots. `moveNodes` mit Ziel-Root re-mapt auf `parentId/folderId=null` (= Drop auf "/"). `patchScope`/`patchNeutralize` mit Synth-Root-Id expandieren ueber die API zu allen Top-Level-Folders+Files und setzen pro Folder zwingend `cascadeChildren=true` (= globale Neutralisierung/Scope-Setzung).
- G5 -- Persistenter Expand-State: in der H-Iteration umgesetzt (siehe unten).
**H-Iteration (zweite Smoke-Runde, gleiche Sitzung)**:
- H1 -- Logik-Audit aller drei Flags. Backend-Verhalten ist konsistent (`_inheritFlags.py`, in einem Modul fuer DS und FDS):
- **Werteliste**: `neutralize` und `ragIndexEnabled` sind `False | True | NULL`. `scope` ist `'personal' | 'mandate' | 'global' | NULL`. `NULL` bedeutet "erbe vom naechsten Vorfahren". `'mixed'` ist nur ein UI-Display-Wert, nie persistiert.
- **Effective-Resolution** (`_resolveWalkValue` / `_resolveWalkValueFds`, `mode='walk'`): own explicit -> ancestor-chain (nahester zuerst) -> default (`False`/`'personal'`). Liefert *immer* einen konkreten Wert.
- **Aggregate-Resolution** (`mode='aggregate'`): wie walk, ABER wenn ein Knoten Nachkommen hat, deren walk-effective-Werte voneinander abweichen, gibt das Backend `'mixed'` zurueck. Reine Frontend-Anzeige.
- **PATCH** (`/api/datasources/{id}/{flag}`): cascade-reset ALLER expliziten Nachkommen-Werte auf `NULL` (bottom-up, deepest first), DANN setzt das Backend den Master-Wert. Konsequenz: nach jedem Toggle hat der Subtree exakt einen expliziten Wert (am Master), alle Nachkommen erben -- aggregate kann gar nicht `'mixed'` werden, solange keine neuen Toggles auf den Nachkommen abgegeben werden.
- **Frontend**: `_handleCycleScope` / `_handleToggleNeutralize` / `_handleToggleRagIndex` mappen `'mixed'` *immer* auf einen konkreten Wert (`'personal'` bzw. `false`). Es gibt keinen Code-Pfad, der `'mixed'` an das Backend uebermittelt. Wenn der User trotz cascade-PATCH `'mixed'` sieht, deutet das auf einen Cascade-Skip im Backend (Bug, kein Designdetail).
- H2/H8 -- *Initial-Render-Bug*: Auto-Expand-Effekt fuehrte sequentielle `for ... await provider.loadChildren(...)` aus. Die erste Antwort triggerte `setNodes`, der Effect-Cleanup setzte `cancelled=true` -- alle weiteren Iterationen brachen ab. Folge: der ZWEITE/DRITTE `defaultExpanded`-Knoten zeigte zwar das Expand-Icon (weil `setExpandedIds` synchron ablief), aber nie Kinder, bis der User manuell collapse+expand klickte. Fix: `Promise.all(...)` parallel + ein einziges, atomares `setNodes(prev => [...prev, ...flat])` pro Effekt-Run. Damit wird das Cancellation-Race irrelevant, weil bis zur Cancel-Flagge alle Antworten schon zugewiesen sind.
- H3 -- *FilesTab Move-404*: `PUT /api/files/{fileId}` (Route-Handler `update_file`) hatte keine `RequestContext`-Dependency und initialisierte `interfaceDbManagement.getInterface(currentUser)` ohne `mandateId`/`featureInstanceId`. RBAC-Filter im `getFile`-Lookup verlieren so ihre Scope-Information, der File ist im "Default-Scope" nicht sichtbar -> 404. Fix: Context-Dep eingefuegt, mandateId+featureInstanceId an `getInterface` durchreichen. Gleicher Fix bei `delete_file`. Zusaetzlich `move_folder` so erweitert, dass es sowohl `parentId` als auch `targetParentId` aus dem Body liest -- der generische `provider.moveNodes(targetParentId)`-Aufruf verwendet die zweite Schreibweise.
- H4 -- *Neuer Folder am falschen Render-Platz*: `FolderFileProvider.createChild(parentId=null, ...)` setzte den FE-`parentId` des erzeugten Nodes auf `null`. `_buildChildMap` mappt `null` auf `__root__`, der neue Folder erschien also auf der Legacy-Top-Level-Reihe NEBEN dem `/`-Synth-Root. Fix: wenn `parentId === null` (kein selektierter Parent, globaler "+"-Klick), `parentId = _SYNTH_ROOT_ID('own')` setzen. Wenn der User auf einen synthetischen Root klickt, wird er ebenfalls als Parent uebernommen. API-Call bleibt `parentId=null`.
- H5 -- *+ Button pro Folder*: neue `onCreateChild?: (parentId: string) => void`-Prop in `TreeNodeRowProps`. Wird gerendert, wenn `provider.createChild` existiert, der Node ein Folder ist und `provider.canCreate?.(node.id)` true ist (oder fehlt). Klick ruft `_createFolderAt(node.id)` -- gleicher Code-Pfad wie der globale "Neuer Ordner"-Button. Erscheint im Hover-Action-Slot vor dem Rename-Pencil.
- H6 -- *FilesTab neutralize teilweise broken*: Diagnose laeuft. Vermutung: gemeinsam mit H2 erledigt -- der Auto-Expand-Race konnte dazu fuehren, dass die optimistic-update-Map `_collectDescendantIds` Knoten verfehlte, deren Children noch nicht im FE-State waren. Mit dem Parallel-Fetch sind alle defaultExpanded-Children synchron zur ersten Render-Phase im Tree.
- H7 -- *Hardcoded "[Persoenliche Quellen]"*: der Backend-Code rief `resolveTextSafe("Persoenliche Quellen")` auf, also den umlaut-encoded Source-String. `t()` gibt fuer DE den Schluessel selbst zurueck (= "Persoenliche Quellen"); fuer andere Sprachen den `[key]`-Fallback. Beides falsch fuer einen Anzeigetext. Fix: Source-String auf `"Persönliche Quellen"` (echter Umlaut) korrigiert -- zeigt im DE-Locale jetzt den lesbaren Text, in EN/FR weiter `[Persönliche Quellen]` (= bekannter i18n-Fallback, klar als untranslated erkennbar).
- H9 -- *FDS Neutralize geht nicht*: `POST /api/workspace/{instanceId}/feature-datasources` hatte einen harten Cross-Mandate-Block: wenn die Workspace-Instance Mandate A war, der referenzierte FeatureInstance aber Mandate B (beide vom User zugaenglich), 403 -> `_ensureRecord` schluckt den Fehler still (`console.error`) und PATCH wird uebersprungen. Sichtbares Symptom: Klick tut nichts. Fix: Cross-Mandate-Check entfernt, statt dessen explizit `getFeatureAccess(userId, body.featureInstanceId)` validieren. `mandateId` der neuen FDS = `wsMandateId` (= Workspace's Mandant), so dass die FDS unter der Workspace-Tenancy lebt -- konsistent mit dem Tree-Filter `workspaceInstanceId == instanceId`.
- H10 (= G5/P5) -- *Persistenter Expand-State*:
- Datamodel: `WorkspaceUserSettings.uiTreeExpansion: Dict[str, List[str]]` (Key = scope-Name wie `'sources'`, `'filesOwn'`, `'filesShared'`).
- Routen: `GET /api/workspace/{instanceId}/ui-tree-expansion/{scope}` -> `{expandedNodes: List[str] | null}`. `PUT /api/workspace/{instanceId}/ui-tree-expansion/{scope}` body `{expandedNodes: List[str]}`. Erste PUT erstellt den Settings-Record, jede weitere PATCHt nur den `uiTreeExpansion[scope]`-Slot.
- Frontend-Hook: `useTreeExpansion(instanceId, scope)` laedt den Initialwert, liefert `{loaded, expandedIds, setExpandedIds}` zurueck. `setExpandedIds` debounct (600 ms) den PUT.
- FormGeneratorTree-Vertrag: zwei neue Props `expandedIds?: string[] | null` + `onExpandedIdsChange?: (ids: string[]) => void`. Bei `Array.isArray(expandedIds)` ueberschreibt die persistierte Liste die Backend-`defaultExpanded`-Hints (User-Praeferenz wins). Bei `null` bleibt der Default-Verhalten erhalten und der erste User-Toggle erstellt den Settings-Record. Tree feuert `onExpandedIdsChange` nur, wenn die neue Liste sich VON der zuletzt vom Parent gepushten unterscheidet (kein Echo-Loop).
- `SourcesTab` und `FilesTab` rufen `useTreeExpansion(instanceId, scope)` und reichen `expandedIds`/`setExpandedIds` an `FormGeneratorTree` weiter. FilesTab verwaltet zwei Scopes (`filesOwn`, `filesShared`) wegen der zwei Tree-Instanzen.
**Diagnose-Befund vor dem Bauen** (Terminal-Log + User-Beobachtung):
- Backend liefert sauber 6 Top-Level-Knoten (`counts={'__root__': 6}` bei
`allDs=38, allFds=11`); Endpoint antwortet 200 OK auf jeden POST.
- User sieht im Tab nur den Ladeplatzhalter `"Lade Datenquellen..."` --
d.h. der State-Pfad im alten `SourcesTab.tsx` aktualisiert das `loadingTopLevel`-
/ `childrenByParent`-State nie. Vermutliche Ursache: Race zwischen StrictMode-
Doppelmount, `mountedRef`-Frueh-Abort und dem zweiten useEffect auf
`expansionSignature` (vier parallele POSTs sichtbar). Diagnose nicht weiter
vertieft, weil der Recovery-Rewrite den ganzen ad-hoc Renderer eliminiert.
- **Konsequenz**: Phase 1 wuerde alleine zwar das Backend "richtig" liefern,
die UI bliebe aber kaputt. Vollstaendiger Recovery-Pfad noetig.
**Plan-Abweichungen (durch Implementierung gelernt)**:
- `TreeNode.displayOrder?: number` als zusaetzliche generische Erweiterung
in Phase 2 noetig: Sortierung von `personalRoot` (alphabetisch nach
`Mandanten-Daten`) sonst falsch. Vorrang vor folder-first / alphabet,
Fallback unveraendert. Kein UDB-Vokabular im Tree.
- Synthetische Container (`synthRoot`, `mandateGroup`) im Provider-Mapping:
`scope`/`neutralize`/`ragIndexEnabled` werden auf `undefined` gesetzt, damit
`FormGeneratorTree` die Buttons gar nicht erst rendert. Sauberer als sie
als readonly zu styling.
- Mandate-Group-Knoten verlieren ihren `mandateRoot`-Parent jetzt korrekt
(vorher `parentKey=None` -> top-level); Test eingeplant.
- `getChildrenForParents` Diagnose-Logging entfernt nach erfolgreichem
Phase-1-Smoke.
## Beschreibung und Kontext
Der "UDB Generic Tree Refactor" (`c-work/4-done/2026-05-udb-generic-tree-refactor.md`,
2026-05-18) ist im Backend sauber geliefert (Helpers + Builder + Endpoint +
Tests 85/85 gruen), aber das Frontend wurde architektur-falsch umgesetzt:
`SourcesTab.tsx` enthaelt eine **eigene, parallele** Tree-Renderer-Implementierung
(`_TreeNodeView`, `_FlagButton`, ~555 Zeilen) anstatt das bereits etablierte
`FormGeneratorTree` zu verwenden, das in `FilesTab.tsx` als Referenz lebt.
Effekt fuer den User:
1. Files-Tab funktioniert (FormGeneratorTree, zeigt Scope-/Neutralize-Icons korrekt).
2. Sources-Tab laedt keine Daten oder rendert nicht das, was erwartet wird.
3. Zwei Tree-Implementierungen muessen parallel gepflegt werden, was die
wiederholten Bugfix-Iterationen erklaert.
Die User-Direktive aus `local/notes/recovery.md` ist klar: alle drei Hierarchien
(Personal Sources, Feature Sources, Folder+Files) **identisch** behandeln. Der
`FormGeneratorTree` ist die Referenz. Sources-Tab muss darauf umgestellt werden.
Risiko bei Nicht-Loesung: weitere Iterationen, weitere Workarounds, fortschreitende
Drift zwischen den beiden Tree-Renderern, wachsende Test-Luecke.
## Fokus und kritische Details
- **Backend ist einzige Quelle der Wahrheit**. UI berechnet NICHTS (keine
Vererbung, keine effektiven Werte, keine Mixed-Aggregation).
- **Keine optimistic UI-Updates**. Toggle-Flow ist strikt:
`Klick -> Spinner an -> PATCH -> Refetch -> Render -> Spinner aus`.
- `FormGeneratorTree` bleibt **domaenenfrei**. Generische Erweiterungen
(Mixed-Symbol, Pending-Spinner, extraActions, optionales Refetch nach Action)
duerfen NICHT von UDB- oder Connection-Vokabular wissen.
- Der Files-Tab nutzt heute optimistic Updates auf scope/neutralize. Diese
bleiben fuer FilesTab erhalten (kein Regress) und werden fuer Sources via
neuer opt-in Prop aktiviert/deaktiviert.
- Backend-Endpoint `POST /api/workspace/{instanceId}/tree/children` liefert
bereits per-Parent Children mit pre-computed `effectiveNeutralize`,
`effectiveScope`, `effectiveRagIndexEnabled` (jeweils `boolean|'mixed'` bzw.
`string|'mixed'`). Das ist der **einzige** Backend-Read-Pfad fuer den
Sources-Tree -- nichts daran aendern.
- Layout-Anforderung (`recovery.md` Section 7): eine sichtbare Quellen-Wurzel,
darunter zwei Sub-Baeume "Persoenliche Quellen" + "Mandanten-Daten". Aktuell
liefert `_topLevel` direkt Connections + Mandate-Groups flach -- es fehlt der
gemeinsame Container.
- Keine Multi-Selection im UDB Sources-Tab.
## Ziel und Nicht-Ziele
### Ziel
1. SourcesTab nutzt `FormGeneratorTree` als alleinigen Renderer. Eigener
`_TreeNodeView` / `_FlagButton`-Code in SourcesTab wird komplett entfernt.
2. Drei Toggle-Flags (neutralize, scope, ragIndexEnabled) erscheinen einheitlich
pro Knoten. Mixed-Symbol einheitlich (`U+25E9`).
3. Toggle-Flow strikt PATCH -> Refetch -> Render. Kein optimistic Update im
Sources-Tab. Spinner pro pendendem Flag-Button.
4. `FormGeneratorTree` erhaelt **eine** generische Erweiterung
(`refreshAfterAction?: boolean`), kennt KEIN UDB-Vokabular.
5. Layout: gemeinsame Wurzel mit zwei Sub-Baeumen (Personal / Mandate).
6. FilesTab bleibt funktional unveraendert (Regression-Schutz).
### Explizit NICHT
- Backend-Helpers (`_inheritFlags.py`, `cascadeReset*`, PATCH-Routes,
Tree-Endpoint) werden **nicht angefasst**.
- Keine neue PATCH-API. Bestehende `/api/datasources/{id}/{scope|neutralize|rag-index}`
reichen.
- Keine FDS-Record-Expansion (User-Entscheidung 2026-05-18: Tabellen-Ebene reicht).
- Kein Mehrfach-Select im SourcesTab.
- Keine Wiederbelebung der geloeschten Tree-Endpoints.
- Keine Aenderung am FilesTab-Verhalten (insbesondere: dort weiterhin keine
Refetch-nach-Action-Pflicht; Files-Modell hat keine Mixed-Aggregation auf
Ancestors, optimistic Update reicht).
## Betroffene Module
- **Gateway**:
- `gateway/modules/serviceCenter/services/serviceKnowledge/_buildTree.py`:
`_topLevel` umbauen, sodass es **eine** synthetische Wurzel `srcRoot` mit
zwei Sub-Knoten zurueckliefert (`personalRoot`, `mandateRoot`); Connections
werden Children von `personalRoot`, Mandate-Groups Children von `mandateRoot`.
Neue Key-Praefixe: `srcRoot`, `personalRoot`, `mandateRoot`. Keine sonstigen
Aenderungen am Builder.
- Optional: `getChildrenForParents` so erweitern, dass die drei neuen Synthese-
Keys gehandhabt werden (Dispatch).
- **Frontend**:
- `frontend_nyla/src/components/UnifiedDataBar/SourcesTab.tsx`:
komplett ersetzen (~555 LOC -> erwartet ~80-120 LOC), nur noch ein duenner
Wrapper um `FormGeneratorTree` mit eigenem Provider und Settings-Modal-Ankopplung.
- `frontend_nyla/src/components/FormGenerator/FormGeneratorTree/types.ts`:
Optional, je nach gewaehltem Approach (siehe Entscheidung E1):
- Variante A: `TreeNode.ragIndexEnabled?: boolean | 'mixed'` als drittes
First-Class-Feld + Provider-Methode `patchRagIndex?(ids, value)`.
- Variante B: RAG ueber `extraActions[]` mit `value: 'mixed'|true|false` --
schon heute unterstuetzt, Tree braucht nichts neues.
- `frontend_nyla/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx`:
eine generische Prop `refreshAfterAction?: boolean` (Default: `false`,
backward-compatible), und ein generischer Refresh-Hook der nach
`_handleCycleScope`/`_handleToggleNeutralize`/`_handleExtraAction` (NUR
wenn aktiviert) `provider.loadChildren(...)` fuer **alle expandierten
Parents + `null`** aufruft und die Nodes ersetzt. Ohne UDB-Vokabular.
- Neuer Datei `frontend_nyla/src/components/UnifiedDataBar/UdbSourcesProvider.ts`:
`TreeNodeProvider`-Implementierung, die `POST /tree/children` aufruft und
Backend-`TreeNode` auf den generischen `FormGeneratorTree.TreeNode` mappt.
- **DB-Migration**: nein.
- **Andere**: keine.
## Entscheidungen
| Datum | Entscheidung | Begruendung |
|-------|--------------|-------------|
| 2026-05-18 | Backend-Helpers, PATCH-Routes und Tree-Endpoint **nicht antasten** | Tests 85/85 gruen, korrekte Cascade-/Mixed-Semantik, recovery.md §11 listet sie als "vorhanden, nicht antasten". |
| 2026-05-18 | SourcesTab eigener Tree-Renderer **wegwerfen** | Architekturverstoss; recovery.md §1.3 + §5 + §9. Parallele Logik = staendige Drift. |
| 2026-05-18 | FilesTab **nicht antasten** | Funktioniert; dient als Regression-Pruefstein fuer FormGeneratorTree-Erweiterungen. |
| 2026-05-18 | E1: RAG als drittes First-Class-Feld in `TreeNode` (Variante A) | Drei Flags muessen "identisch behandelt" werden (recovery.md §1, §2). Wenn scope+neutralize First-Class sind, gehoert ragIndexEnabled auch dazu. ExtraActions sind ein Workaround und schaffen Asymmetrie. Tree-Renderer kennt nur "ein RAG-Flag mit Mixed-Semantik" -- kein UDB-Vokabular. |
| 2026-05-18 | E2: `refreshAfterAction: boolean` als opt-in Prop am Tree | FilesTab bleibt unveraendert (default `false` = optimistic local update); Sources opted-in (`true` = refetch all expanded). Generisch, kein UDB-Wissen im Tree. |
| 2026-05-18 | E3: Synthetische Root-Knoten `srcRoot` + `personalRoot` + `mandateRoot` im Builder | recovery.md §7 verlangt eine sichtbare Quellen-Wurzel mit zwei Sub-Baeumen. Backend bleibt autoritativ; UI mappt 1:1. |
## Umsetzungs-Checkliste
### Phase 1: Backend (Layout-Synthese) -- DONE
- [x] In `_buildTree.py` neue Key-Praefixe einfuehren (`srcRoot`, `personalRoot`, `mandateRoot`)
als literal-konstante Top-Level-Tokens (kein `_encode`).
- [x] `_topLevel(...)` umgebaut: liefert genau einen `srcRoot` mit
`hasChildren=True`, `displayOrder=0`. Personal- und Mandate-Listen werden
nicht mehr im Top-Level emittiert.
- [x] Neue Helper `_srcRootChildren()` (statisches `[personalRoot, mandateRoot]`),
`_personalRootChildren(instanceId, context, allDs)` (Connections mit
`parentKey=personalRoot`), `_mandateRootChildren(...)` (Mandate-Groups
mit `parentKey=mandateRoot`).
- [x] `_syntheticNode(...)` + `_emptyTriplet()` Helpers fuer konsistente
Container-Defaults. `displayOrder` als generischer Sort-Hint.
- [x] `getChildrenForParents` Dispatch fuer die drei neuen Keys (Vergleich
auf literalen `parentKey` BEVOR `_decode` ausgewertet wird).
- [x] **Keine** Aenderung an `_inheritFlags.py`, `routeDataSources.py`,
`routeFeatureWorkspace.py`. Synthese-Knoten haben keinen DB-Record und
tauchen daher nicht in der Effective-Aggregation auf.
- [x] Tests in `test_buildTree.py` ergaenzt: 7 neue Cases unter `TestSyntheticRoots`
(Top-Level immer 1 srcRoot; srcRoot expandiert auf 2 Container mit
`displayOrder` 0/1; neutrale Defaults; Personal-Root emittiert aktive
Connections; Mandate-Root filtert Mandate ohne Data-Features). Bestehende
Top-Level-Test angepasst.
### Phase 2: FormGeneratorTree (generische Erweiterung, kein UDB-Vokabular) -- DONE
- [x] `types.ts`: `TreeNode.ragIndexEnabled?: boolean | 'mixed'` ergaenzt.
- [x] `types.ts`: `TreeNode.displayOrder?: number` ergaenzt (Plan-Erweiterung,
wegen Sortierung der Synthese-Container noetig; vorrang vor folder-first).
- [x] `types.ts`: `TreeNodeProvider.patchRagIndex?(ids, value): Promise<void>`
und `canPatchRagIndex?(node): boolean` ergaenzt.
- [x] `types.ts`: `FormGeneratorTreeProps.refreshAfterAction?: boolean`
(default `false`, Backward-Compat fuer FilesTab).
- [x] `FormGeneratorTree.tsx`: dritter Flag-Button (`_RAG_EMOJI`,
`_ACTION_RAG`) rendert genau dann, wenn `node.ragIndexEnabled !==
undefined`. Mixed-Symbol identisch zu scope/neutralize.
- [x] `_handleToggleRagIndex` analog zu `_handleToggleNeutralize` -- mit
gleichem Cascade-auf-Folder-Optimismus, der jedoch bei
`refreshAfterAction=true` uebersprungen wird.
- [x] Sort-Comparator in `_buildChildMap` erweitert: bei Geschwistern mit
`displayOrder` sortiert numerisch aufsteigend; Knoten ohne `displayOrder`
kommen NACH Knoten mit. Vorrang vor folder-first.
- [x] `_refetchAllExpanded()` Helper: parallele `loadChildren(p, ownership)`
fuer `p in [null, ...expandedIds]`, danach atomar `setNodes(prev)`
mit Filter (Knoten unter neuen Parents werden ersetzt; alle anderen
bleiben unangetastet).
- [x] `_runAction` ruft `_refetchAllExpanded` automatisch auf wenn
`refreshAfterAction=true`. Spinner haelt durch bis Refetch fertig.
- [x] FilesTab: nicht angefasst. Default `refreshAfterAction=false` =
bisheriger optimistic-update-Pfad.
- [x] Tests `__tests__/FormGeneratorTree.test.tsx` ergaenzt um 10 neue
Cases: RAG-Default-Render, RAG-Click->patchRagIndex, RAG hidden ohne
Feld, RAG-Mixed-Symbol, RAG-Mixed cycles to false, displayOrder
numerisch, displayOrder vor unsortiertem, Fallback auf folder-first,
`refreshAfterAction=true` triggert Refetch, `refreshAfterAction=false`
triggert keinen Refetch.
### Phase 3: SourcesTab (Wegwerfen + Wrapper) -- DONE
- [x] Neue Datei `UnifiedDataBar/UdbSourcesProvider.tsx`:
- `rootKey: 'udb-sources-${instanceId}'`.
- `loadChildren(parentId, _ownership)`: POST `/api/workspace/{instanceId}/tree/children`
mit `{parents: [parentId]}`; mappt `nodesByParent[parentId ?? '__root__']`
auf `TreeNode<UdbBackendNode>` und befuellt internen `nodeCache: Map<key,
UdbBackendNode>`. Cache wird von Patch-Pfaden gelesen.
- Mapping wie geplant; **Synthese-Container** (`synthRoot`, `mandateGroup`)
bekommen `scope=neutralize=ragIndexEnabled=undefined` damit `FormGeneratorTree`
die Buttons gar nicht erst rendert.
- `canPatchScope`/`canPatchNeutralize` -> `!_isSyntheticContainer(kind)`.
- `canPatchRagIndex` -> `!_isSyntheticContainer && data.supportsRag`.
- `patchScope`/`patchNeutralize`/`patchRagIndex` ueber gemeinsame `_patchFlag`-
Helper: lookup im `nodeCache`, `_ensureRecord` (POST `/datasources` oder
`/feature-datasources` je nach `kind`), dann PATCH des Flags. URL-Pfad
`rag-index` (mit Bindestrich), Body `{ragIndexEnabled: ...}` (camelCase).
- `cascadeChildren`-Param ignoriert (Backend cascadiert ueberall).
- `getBatchActions` nicht implementiert (kein Multi-Select).
- `canCreate`/`canRename`/`canDelete`/`canMove` nicht implementiert -> Tree
rendert keine Aktionen dafuer.
- [x] `UnifiedDataBar/SourcesTab.tsx` komplett neu, ~75 LOC:
- `useMemo(() => createUdbSourcesProvider(instanceId, _handleOpenSettings), [...])`.
- `<FormGeneratorTree provider compact selectable={false}
allowCreateFolder={false} refreshAfterAction />`.
- Settings-Modal-Anbindung ueber `extraActions` auf jedem Node mit
`dataSourceId`. Klick auf Settings setzt lokalen Modal-State im Wrapper.
- [x] Alter `_TreeNodeView`/`_FlagButton` ad-hoc Renderer komplett entfernt.
- [x] `SourcesTab.module.css` wurde im Vorgaenger-Refactor schon entfernt --
keine Wiedereinfuehrung noetig.
- [x] Tests `__tests__/UdbSourcesProvider.test.ts`: 14 Cases. loadChildren-
Endpoint, Mapping, Synthese-Container ohne Flags, RAG-Hide ohne
`supportsRag`, settings-extraAction, Cache-Befuellung, canPatch*
Predicates, patchScope mit existierendem Record, ensureRecord-Pfad,
cache-miss skip, patchNeutralize, patchRagIndex (DataSource & FeatureDataSource).
### Phase 4: Validierung -- in progress
- [x] Backend-Restart: ueberprueft via Diagnose-Log (4× POST 200 OK,
`counts={'__root__': 6}` -> mit neuem `_topLevel` aendert sich das auf
`counts={'__root__': 1}`); Diagnose-Log danach entfernt.
- [x] Backend-Suite gruen: `pytest tests/unit/services/test_buildTree.py
tests/unit/services/test_inheritFlags.py` -> 89/89.
- [x] Frontend-Suite gruen: `npx vitest run src/components/UnifiedDataBar
src/components/FormGenerator/FormGeneratorTree` -> 65/65.
- [ ] User-Smoke: Top-Level zeigt einen "Datenquellen"-Knoten mit Chevron.
- [ ] User-Smoke: Expandieren -> "Persoenliche Quellen" + "Mandanten-Daten".
- [ ] User-Smoke: Persoenliche Quellen: pro UserConnection ein Knoten mit
Authority-Icon + drei Flag-Buttons (RAG, Scope, Neutralize) + Settings-Icon.
- [ ] User-Smoke: Mandanten-Daten: pro Mandate-Group + pro Feature-Instanz
+ pro Tabelle jeweils drei Flag-Buttons.
- [ ] User-Smoke: Klick auf Flag -> Spinner ueber Button bis PATCH+Refetch
durch sind. Andere Buttons bleiben klickbar.
- [ ] User-Smoke: Klick auf Mixed-Symbol setzt deterministischen Wert,
Cascade an Children sichtbar nach Refetch.
- [ ] User-Smoke: FilesTab unveraendertes Verhalten.
- [ ] **Wiki-Update** (siehe Abschluss).
### Phase 5: RBAC / Permissions / Neutralisierung / Navigation / Billing
- [ ] **RBAC**: nichts neu. Personal-Sources sind owner-scoped (`userId`-Filter
bleibt im Builder), Feature-Sources via `getFeatureAccess.enabled` -- alles
schon im bestehenden Builder. Keine neuen Rollen, keine Mandate-/Feature-
Rollen-Mischung (recovery.md §10 + `rbac-role-separation.mdc`).
- [ ] **Neutralisierung**: betroffen nur insofern, als der Toggle das `neutralize`-
Flag korrekt rendert. Logik im Backend, kein FE-Pfad.
- [ ] **Navigation / Routing**: keine Aenderung. UDB ist eine Komponente,
keine Page.
- [ ] **Billing-Impact**: keiner.
## Akzeptanzkriterien
| # | Kriterium (Given-When-Then) | Prio |
|---|----------------------------|------|
| 1 | Given ein User mit aktiven Connections und Feature-Instanzen, When er den UDB-Sources-Tab oeffnet, Then sieht er **einen** Wurzel-Knoten "Datenquellen" mit Chevron, der zwei Geschwister enthaelt: "Persoenliche Quellen" und "Mandanten-Daten". | must |
| 2 | Given ein Knoten mit drei Flags, When der User auf einen Flag-Button klickt, Then erscheint ueber genau diesem Button ein Spinner, der erst verschwindet, nachdem (a) das PATCH zurueckgekehrt ist und (b) der Refetch fuer alle expandierten Parents abgeschlossen ist. | must |
| 3 | Given ein Connection-Knoten mit drei expandierten Service-Children und divergenten neutralize-Werten, When der User auf das Mixed-Symbol des Connection-Knotens klickt, Then setzt das Backend einen expliziten Wert + Cascade-Reset auf Descendants, und der Tree zeigt nach Refetch fuer **alle** sichtbaren Knoten die neuen effective-Werte (kein Stand-Wert). | must |
| 4 | Given ein FilesTab-User, When er Scope/Neutralize toggelt oder Files dragt-und-droppt, Then verhaelt sich der Tab unveraendert zur heutigen Version (Regression-Schutz: kein neuer Refetch, keine veraenderten Icons, kein neues Verhalten). | must |
| 5 | Given das FormGeneratorTree-Modul, When ein Reviewer den Diff prueft, Then enthaelt der Tree-Code keinen UDB-, Connection-, DataSource- oder FeatureDataSource-spezifischen String oder Konstante. | must |
| 6 | Given das SourcesTab-Modul, When ein Reviewer den Diff prueft, Then ist der View ein duenner Wrapper (< 150 LOC), enthaelt keinen rekursiven Tree-Renderer und keinen Flag-Button-Code. | must |
| 7 | Given ein Knoten ohne Backing-DB-Record (z.B. Folder, der noch nie getoggelt wurde), When der User auf einen Flag-Button klickt, Then erstellt der Provider den Record via `POST /datasources` oder `/feature-datasources`, danach erst die PATCH-Operation, ohne UI-Glitch. | must |
| 8 | Given die Backend-Tests, When CI laeuft, Then sind `test_inheritFlags.py` + `test_buildTree.py` weiter gruen (mind. 85/85, plus die 3-4 neuen Tests fuer die Synthese-Wurzel). | must |
| 9 | Given das Frontend, When `npx tsc --noEmit --skipLibCheck` und `npm run build` laufen, Then kompiliert ohne Fehler/Warnings. | must |
| 10 | Given ein User mit drei Browser-Tabs auf der gleichen Seite, When er in einem Tab toggelt, Then die anderen Tabs zeigen den neuen Stand erst nach manuellem Reload (kein WebSocket noetig, kein Multi-Tab-Sync gefordert). | should |
## Testplan
| ID | AC | Art | Automatisiert | Repo-Pfad | Status |
|----|----|-----|--------------|-----------|--------|
| T1 | 1, 8 | unit | ja | `gateway/tests/unit/services/test_buildTree.py::TestSyntheticRoots` | DONE -- 7/7 |
| T2 | 8 | unit | ja | `gateway/tests/unit/services/test_buildTree.py::TestGetChildrenForParents` (regress.) | DONE -- 2/2 |
| T3 | 8 | unit | ja | `gateway/tests/unit/services/test_inheritFlags.py` (regress.) | DONE -- 72/72 |
| T4 | 2, 3 | unit | ja | `FormGeneratorTree.test.tsx::refreshAfterAction` | DONE -- 2/2 |
| T5 | 5 | unit | ja | `FormGeneratorTree.test.tsx::RAG-Index toggle` (5) + `displayOrder` (3) | DONE -- 8/8 |
| T6 | 4 | unit | ja | bestehende `FormGeneratorTree.test.tsx` Tests (Regression) | DONE -- 38/38 |
| T7 | 6 | manual | nein | `SourcesTab.tsx` ist 75 LOC, kein rekursiver Renderer, kein Flag-Button-Code (nur `<FormGeneratorTree refreshAfterAction selectable={false} />` + Settings-Modal) | DONE |
| T8 | 1, 2, 3, 7 | manual | nein | Smoke-Test: lokale Instanz, Toggle-Cascade pruefen, Mixed-Klick pruefen, neuen Knoten toggeln | OPEN |
| T9 | 9 | build | ja | `cd frontend_nyla && npx tsc --noEmit --skipLibCheck && npm run build` | OPEN |
## Links
- recovery.md (Anforderungen): `local/notes/recovery.md`
- Vorgaenger-Plan (done, FE-Teil bricht): `wiki/c-work/4-done/2026-05-udb-generic-tree-refactor.md`
- Vorgaenger Cascade-Inherit: `wiki/c-work/4-done/2026-05-udb-cascade-inherit.md`
- Settings-Modal-Plan: `wiki/c-work/4-done/2026-05-udb-datasource-settings.md`
- Backend-Helpers (nicht antasten): `gateway/modules/serviceCenter/services/serviceKnowledge/_inheritFlags.py`, `_buildTree.py`
- Backend-Routes (nicht antasten): `gateway/modules/routes/routeDataSources.py`, `gateway/modules/features/workspace/routeFeatureWorkspace.py`
- Tree-Komponente: `frontend_nyla/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx`
- Referenz-Implementierung (nicht antasten): `frontend_nyla/src/components/UnifiedDataBar/FilesTab.tsx`
- Wegzuwerfender Code: `frontend_nyla/src/components/UnifiedDataBar/SourcesTab.tsx`
- PR: noch nicht erstellt.
## Abschluss
- [ ] `b-reference/frontend-nyla/formgenerator.md` aktualisieren: Abschnitt
"FormGeneratorTree" um die neuen Props (`refreshAfterAction`,
`node.ragIndexEnabled`, `provider.patchRagIndex`) ergaenzen.
- [ ] `b-reference/frontend-nyla/architecture.md` aktualisieren: UDB-Abschnitt
so umschreiben, dass SourcesTab als duenner Wrapper um FormGeneratorTree
beschrieben ist; Files-Tab bleibt unveraendert beschrieben.
- [ ] `b-reference/platform/neutralization.md` Querschnitt pruefen
(UDB-Toggle-Flow auf Sources verweist nun auf FormGeneratorTree-Pfad).
- [ ] `TOPICS.md`: bestehender Eintrag "UDB Generic Tree Refactor" um Hinweis
auf den Recovery-Plan ergaenzen, Verweis auf neue Doku-Stellen.
- [ ] `c-work/_CHANGELOG.md`: eine Zeile Refactor pro Phase.
- [ ] Dieses Dokument -> `4-done/` verschieben.