33 KiB
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 lockvsopen lock,brainvsthought-bubble. (Cascade-Logik im Backend identisch zuscope, voncascadeResetDescendants(rec, flag)parametrisch. Test-Suite belegt das inTestCascadeResetmitflag="neutralize".) - F2 -- Top-Level-Layout flach:
srcRootundmandateRootentfernt. Top-Level emittiert nun direkt[personalRoot, mgrp|m1, mgrp|m2, ...]. Mandate-Groups sind keine Children eines Wrappers mehr. Tree-Header kommt aus demtitle-Prop vonFormGeneratorTree. - F3 --
TreeNode.defaultExpandedals generisches Tree-Feature: einmal pro Node-Id auto-expand bei initialem Load (one-shot viaautoExpandedRef). Backend markiertpersonalRootund alle Mandate-Groups mitdefaultExpanded=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 imUdbSourcesProvidernur noch fuerkind in {connection, featureNode}(= Daten-Quellen-Root) angehaengt -- aber dort immer, auch ohnedataSourceId. Klick triggert_ensureRecord->onOpenSettings(dsId, label). - F5 --
SourcesTabsetzttitle={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 auftsconfig.test.json(LSP fiel fuer*.test.tsxauf den App-Config zurueck, der Tests ausdruecklichexcluded). Fix:tsconfig.json.referencesum./tsconfig.test.jsonerweitert + im Testfile expliziterimport React,import 'vitest/globals'-aequivalent (@testing-library/jest-dom/vitest),afterEachausvitest. 139 -> 0; Tests 50/50 weiterhin gruen. - G2 -- Off-State-Differenzierung: offenes Schloss + RAG-OFF (Brain) bekommen
filter: grayscale(1); opacity: .45als inline-style. ON-Zustand bleibt voll farbig. Eindeutige Unterscheidung auf einen Blick. - G3 -- Sub-Element-Expansion:
- Bug:
_browseChildrensetzteparentKeyauf die synthesisierte ds-Koordinate (ds|conn|sourceType|/), aber der Anrufer hat densvc|...-Key erwartet. Folge: zurueckgegebene Folders/Files hatten einenparentId, der nicht im Tree existiert; sie wurden stillschweigend verworfen. Fix:_browseChildrenakzeptiert jetztparentKeyund nutzt ihn fuer Childs. - Feature-Felder:
fdsTablehat jetzthasChildren: Truewenn das DataObject-Metafields: List[str]deklariert. Neuer Knoten-TypfdsField(kind), neuer Dispatcher-Zweigfdstbl|fi|table->_featureTableFields. Effektiver Neutralize-Wert pro Feld =parent.neutralize OR field IN parent.neutralizeFields. Toggle wird imUdbSourcesProviderper Special-Case aufPATCH /api/datasources/{id}/neutralize-fieldsmit der mutierten Liste umgesetzt;neutralizeFieldsist im fdsTable-Payload mit drin (kein Extra-GET). Scope und RAG auf Feld-Ebene sind bewusst nicht editierbar.
- Bug:
- G4 -- FilesTab: synthetischer "/"-Root pro Ownership. Provider gibt fuer
parentId=nulleinen__filesRoot:<ownership>-Knoten zurueck (defaultExpanded=true); reale Top-Level-Items werden Children dieses Roots.moveNodesmit Ziel-Root re-mapt aufparentId/folderId=null(= Drop auf "/").patchScope/patchNeutralizemit Synth-Root-Id expandieren ueber die API zu allen Top-Level-Folders+Files und setzen pro Folder zwingendcascadeChildren=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:
neutralizeundragIndexEnabledsindFalse | True | NULL.scopeist'personal' | 'mandate' | 'global' | NULL.NULLbedeutet "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 aufNULL(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/_handleToggleRagIndexmappen'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).
- Werteliste:
- H2/H8 -- Initial-Render-Bug: Auto-Expand-Effekt fuehrte sequentielle
for ... await provider.loadChildren(...)aus. Die erste Antwort triggertesetNodes, der Effect-Cleanup setztecancelled=true-- alle weiteren Iterationen brachen ab. Folge: der ZWEITE/DRITTEdefaultExpanded-Knoten zeigte zwar das Expand-Icon (weilsetExpandedIdssynchron ablief), aber nie Kinder, bis der User manuell collapse+expand klickte. Fix:Promise.all(...)parallel + ein einziges, atomaressetNodes(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-Handlerupdate_file) hatte keineRequestContext-Dependency und initialisierteinterfaceDbManagement.getInterface(currentUser)ohnemandateId/featureInstanceId. RBAC-Filter imgetFile-Lookup verlieren so ihre Scope-Information, der File ist im "Default-Scope" nicht sichtbar -> 404. Fix: Context-Dep eingefuegt, mandateId+featureInstanceId angetInterfacedurchreichen. Gleicher Fix beidelete_file. Zusaetzlichmove_folderso erweitert, dass es sowohlparentIdals auchtargetParentIdaus dem Body liest -- der generischeprovider.moveNodes(targetParentId)-Aufruf verwendet die zweite Schreibweise. - H4 -- Neuer Folder am falschen Render-Platz:
FolderFileProvider.createChild(parentId=null, ...)setzte den FE-parentIddes erzeugten Nodes aufnull._buildChildMapmapptnullauf__root__, der neue Folder erschien also auf der Legacy-Top-Level-Reihe NEBEN dem/-Synth-Root. Fix: wennparentId === 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 bleibtparentId=null. - H5 -- + Button pro Folder: neue
onCreateChild?: (parentId: string) => void-Prop inTreeNodeRowProps. Wird gerendert, wennprovider.createChildexistiert, der Node ein Folder ist undprovider.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
_collectDescendantIdsKnoten 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-datasourceshatte einen harten Cross-Mandate-Block: wenn die Workspace-Instance Mandate A war, der referenzierte FeatureInstance aber Mandate B (beide vom User zugaenglich), 403 ->_ensureRecordschluckt den Fehler still (console.error) und PATCH wird uebersprungen. Sichtbares Symptom: Klick tut nichts. Fix: Cross-Mandate-Check entfernt, statt dessen explizitgetFeatureAccess(userId, body.featureInstanceId)validieren.mandateIdder neuen FDS =wsMandateId(= Workspace's Mandant), so dass die FDS unter der Workspace-Tenancy lebt -- konsistent mit dem Tree-FilterworkspaceInstanceId == 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 denuiTreeExpansion[scope]-Slot. - Frontend-Hook:
useTreeExpansion(instanceId, scope)laedt den Initialwert, liefert{loaded, expandedIds, setExpandedIds}zurueck.setExpandedIdsdebounct (600 ms) den PUT. - FormGeneratorTree-Vertrag: zwei neue Props
expandedIds?: string[] | null+onExpandedIdsChange?: (ids: string[]) => void. BeiArray.isArray(expandedIds)ueberschreibt die persistierte Liste die Backend-defaultExpanded-Hints (User-Praeferenz wins). Beinullbleibt der Default-Verhalten erhalten und der erste User-Toggle erstellt den Settings-Record. Tree feuertonExpandedIdsChangenur, wenn die neue Liste sich VON der zuletzt vom Parent gepushten unterscheidet (kein Echo-Loop). SourcesTabundFilesTabrufenuseTreeExpansion(instanceId, scope)und reichenexpandedIds/setExpandedIdsanFormGeneratorTreeweiter. FilesTab verwaltet zwei Scopes (filesOwn,filesShared) wegen der zwei Tree-Instanzen.
- Datamodel:
Diagnose-Befund vor dem Bauen (Terminal-Log + User-Beobachtung):
- Backend liefert sauber 6 Top-Level-Knoten (
counts={'__root__': 6}beiallDs=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 altenSourcesTab.tsxaktualisiert dasloadingTopLevel- /childrenByParent-State nie. Vermutliche Ursache: Race zwischen StrictMode- Doppelmount,mountedRef-Frueh-Abort und dem zweiten useEffect aufexpansionSignature(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?: numberals zusaetzliche generische Erweiterung in Phase 2 noetig: Sortierung vonpersonalRoot(alphabetisch nachMandanten-Daten) sonst falsch. Vorrang vor folder-first / alphabet, Fallback unveraendert. Kein UDB-Vokabular im Tree.- Synthetische Container (
synthRoot,mandateGroup) im Provider-Mapping:scope/neutralize/ragIndexEnabledwerden aufundefinedgesetzt, damitFormGeneratorTreedie Buttons gar nicht erst rendert. Sauberer als sie als readonly zu styling. - Mandate-Group-Knoten verlieren ihren
mandateRoot-Parent jetzt korrekt (vorherparentKey=None-> top-level); Test eingeplant. getChildrenForParentsDiagnose-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:
- Files-Tab funktioniert (FormGeneratorTree, zeigt Scope-/Neutralize-Icons korrekt).
- Sources-Tab laedt keine Daten oder rendert nicht das, was erwartet wird.
- 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. FormGeneratorTreebleibt 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/childrenliefert bereits per-Parent Children mit pre-computedeffectiveNeutralize,effectiveScope,effectiveRagIndexEnabled(jeweilsboolean|'mixed'bzw.string|'mixed'). Das ist der einzige Backend-Read-Pfad fuer den Sources-Tree -- nichts daran aendern. - Layout-Anforderung (
recovery.mdSection 7): eine sichtbare Quellen-Wurzel, darunter zwei Sub-Baeume "Persoenliche Quellen" + "Mandanten-Daten". Aktuell liefert_topLeveldirekt Connections + Mandate-Groups flach -- es fehlt der gemeinsame Container. - Keine Multi-Selection im UDB Sources-Tab.
Ziel und Nicht-Ziele
Ziel
- SourcesTab nutzt
FormGeneratorTreeals alleinigen Renderer. Eigener_TreeNodeView/_FlagButton-Code in SourcesTab wird komplett entfernt. - Drei Toggle-Flags (neutralize, scope, ragIndexEnabled) erscheinen einheitlich
pro Knoten. Mixed-Symbol einheitlich (
U+25E9). - Toggle-Flow strikt PATCH -> Refetch -> Render. Kein optimistic Update im Sources-Tab. Spinner pro pendendem Flag-Button.
FormGeneratorTreeerhaelt eine generische Erweiterung (refreshAfterAction?: boolean), kennt KEIN UDB-Vokabular.- Layout: gemeinsame Wurzel mit zwei Sub-Baeumen (Personal / Mandate).
- 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:_topLevelumbauen, sodass es eine synthetische WurzelsrcRootmit zwei Sub-Knoten zurueckliefert (personalRoot,mandateRoot); Connections werden Children vonpersonalRoot, Mandate-Groups Children vonmandateRoot. Neue Key-Praefixe:srcRoot,personalRoot,mandateRoot. Keine sonstigen Aenderungen am Builder.- Optional:
getChildrenForParentsso 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 umFormGeneratorTreemit 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-MethodepatchRagIndex?(ids, value). - Variante B: RAG ueber
extraActions[]mitvalue: 'mixed'|true|false-- schon heute unterstuetzt, Tree braucht nichts neues.
- Variante A:
frontend_nyla/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx: eine generische ProprefreshAfterAction?: boolean(Default:false, backward-compatible), und ein generischer Refresh-Hook der nach_handleCycleScope/_handleToggleNeutralize/_handleExtraAction(NUR wenn aktiviert)provider.loadChildren(...)fuer alle expandierten Parents +nullaufruft und die Nodes ersetzt. Ohne UDB-Vokabular.- Neuer Datei
frontend_nyla/src/components/UnifiedDataBar/UdbSourcesProvider.ts:TreeNodeProvider-Implementierung, diePOST /tree/childrenaufruft und Backend-TreeNodeauf den generischenFormGeneratorTree.TreeNodemappt.
- 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
- In
_buildTree.pyneue Key-Praefixe einfuehren (srcRoot,personalRoot,mandateRoot) als literal-konstante Top-Level-Tokens (kein_encode). _topLevel(...)umgebaut: liefert genau einensrcRootmithasChildren=True,displayOrder=0. Personal- und Mandate-Listen werden nicht mehr im Top-Level emittiert.- Neue Helper
_srcRootChildren()(statisches[personalRoot, mandateRoot]),_personalRootChildren(instanceId, context, allDs)(Connections mitparentKey=personalRoot),_mandateRootChildren(...)(Mandate-Groups mitparentKey=mandateRoot). _syntheticNode(...)+_emptyTriplet()Helpers fuer konsistente Container-Defaults.displayOrderals generischer Sort-Hint.getChildrenForParentsDispatch fuer die drei neuen Keys (Vergleich auf literalenparentKeyBEVOR_decodeausgewertet wird).- Keine Aenderung an
_inheritFlags.py,routeDataSources.py,routeFeatureWorkspace.py. Synthese-Knoten haben keinen DB-Record und tauchen daher nicht in der Effective-Aggregation auf. - Tests in
test_buildTree.pyergaenzt: 7 neue Cases unterTestSyntheticRoots(Top-Level immer 1 srcRoot; srcRoot expandiert auf 2 Container mitdisplayOrder0/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
types.ts:TreeNode.ragIndexEnabled?: boolean | 'mixed'ergaenzt.types.ts:TreeNode.displayOrder?: numberergaenzt (Plan-Erweiterung, wegen Sortierung der Synthese-Container noetig; vorrang vor folder-first).types.ts:TreeNodeProvider.patchRagIndex?(ids, value): Promise<void>undcanPatchRagIndex?(node): booleanergaenzt.types.ts:FormGeneratorTreeProps.refreshAfterAction?: boolean(defaultfalse, Backward-Compat fuer FilesTab).FormGeneratorTree.tsx: dritter Flag-Button (_RAG_EMOJI,_ACTION_RAG) rendert genau dann, wennnode.ragIndexEnabled !== undefined. Mixed-Symbol identisch zu scope/neutralize._handleToggleRagIndexanalog zu_handleToggleNeutralize-- mit gleichem Cascade-auf-Folder-Optimismus, der jedoch beirefreshAfterAction=trueuebersprungen wird.- Sort-Comparator in
_buildChildMaperweitert: bei Geschwistern mitdisplayOrdersortiert numerisch aufsteigend; Knoten ohnedisplayOrderkommen NACH Knoten mit. Vorrang vor folder-first. _refetchAllExpanded()Helper: paralleleloadChildren(p, ownership)fuerp in [null, ...expandedIds], danach atomarsetNodes(prev)mit Filter (Knoten unter neuen Parents werden ersetzt; alle anderen bleiben unangetastet)._runActionruft_refetchAllExpandedautomatisch auf wennrefreshAfterAction=true. Spinner haelt durch bis Refetch fertig.- FilesTab: nicht angefasst. Default
refreshAfterAction=false= bisheriger optimistic-update-Pfad. - Tests
__tests__/FormGeneratorTree.test.tsxergaenzt 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=truetriggert Refetch,refreshAfterAction=falsetriggert keinen Refetch.
Phase 3: SourcesTab (Wegwerfen + Wrapper) -- DONE
- Neue Datei
UnifiedDataBar/UdbSourcesProvider.tsx:rootKey: 'udb-sources-${instanceId}'.loadChildren(parentId, _ownership): POST/api/workspace/{instanceId}/tree/childrenmit{parents: [parentId]}; mapptnodesByParent[parentId ?? '__root__']aufTreeNode<UdbBackendNode>und befuellt internennodeCache: Map<key, UdbBackendNode>. Cache wird von Patch-Pfaden gelesen.- Mapping wie geplant; Synthese-Container (
synthRoot,mandateGroup) bekommenscope=neutralize=ragIndexEnabled=undefineddamitFormGeneratorTreedie Buttons gar nicht erst rendert. canPatchScope/canPatchNeutralize->!_isSyntheticContainer(kind).canPatchRagIndex->!_isSyntheticContainer && data.supportsRag.patchScope/patchNeutralize/patchRagIndexueber gemeinsame_patchFlag- Helper: lookup imnodeCache,_ensureRecord(POST/datasourcesoder/feature-datasourcesje nachkind), dann PATCH des Flags. URL-Pfadrag-index(mit Bindestrich), Body{ragIndexEnabled: ...}(camelCase).cascadeChildren-Param ignoriert (Backend cascadiert ueberall).getBatchActionsnicht implementiert (kein Multi-Select).canCreate/canRename/canDelete/canMovenicht implementiert -> Tree rendert keine Aktionen dafuer.
UnifiedDataBar/SourcesTab.tsxkomplett neu, ~75 LOC:useMemo(() => createUdbSourcesProvider(instanceId, _handleOpenSettings), [...]).<FormGeneratorTree provider compact selectable={false} allowCreateFolder={false} refreshAfterAction />.- Settings-Modal-Anbindung ueber
extraActionsauf jedem Node mitdataSourceId. Klick auf Settings setzt lokalen Modal-State im Wrapper.
- Alter
_TreeNodeView/_FlagButtonad-hoc Renderer komplett entfernt. SourcesTab.module.csswurde im Vorgaenger-Refactor schon entfernt -- keine Wiedereinfuehrung noetig.- Tests
__tests__/UdbSourcesProvider.test.ts: 14 Cases. loadChildren- Endpoint, Mapping, Synthese-Container ohne Flags, RAG-Hide ohnesupportsRag, settings-extraAction, Cache-Befuellung, canPatch* Predicates, patchScope mit existierendem Record, ensureRecord-Pfad, cache-miss skip, patchNeutralize, patchRagIndex (DataSource & FeatureDataSource).
Phase 4: Validierung -- in progress
- Backend-Restart: ueberprueft via Diagnose-Log (4× POST 200 OK,
counts={'__root__': 6}-> mit neuem_topLevelaendert sich das aufcounts={'__root__': 1}); Diagnose-Log danach entfernt. - Backend-Suite gruen:
pytest tests/unit/services/test_buildTree.py tests/unit/services/test_inheritFlags.py-> 89/89. - 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 viagetFeatureAccess.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.mdaktualisieren: Abschnitt "FormGeneratorTree" um die neuen Props (refreshAfterAction,node.ragIndexEnabled,provider.patchRagIndex) ergaenzen.b-reference/frontend-nyla/architecture.mdaktualisieren: UDB-Abschnitt so umschreiben, dass SourcesTab als duenner Wrapper um FormGeneratorTree beschrieben ist; Files-Tab bleibt unveraendert beschrieben.b-reference/platform/neutralization.mdQuerschnitt 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.