# 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:`-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` 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` und befuellt internen `nodeCache: Map`. 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), [...])`. - ``. - 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 `` + 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.