wiki/c-work/4-done/2026-05-udb-sources-recovery.md

33 KiB
Raw Blame History

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 excluded). 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:
    • platform-core/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:
    • ui-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.
    • ui-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.
    • ui-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 ui-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

  • In _buildTree.py neue Key-Praefixe einfuehren (srcRoot, personalRoot, mandateRoot) als literal-konstante Top-Level-Tokens (kein _encode).
  • _topLevel(...) umgebaut: liefert genau einen srcRoot mit hasChildren=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 mit parentKey=personalRoot), _mandateRootChildren(...) (Mandate-Groups mit parentKey=mandateRoot).
  • _syntheticNode(...) + _emptyTriplet() Helpers fuer konsistente Container-Defaults. displayOrder als generischer Sort-Hint.
  • getChildrenForParents Dispatch fuer die drei neuen Keys (Vergleich auf literalen parentKey BEVOR _decode ausgewertet 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.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

  • types.ts: TreeNode.ragIndexEnabled?: boolean | 'mixed' ergaenzt.
  • types.ts: TreeNode.displayOrder?: number ergaenzt (Plan-Erweiterung, wegen Sortierung der Synthese-Container noetig; vorrang vor folder-first).
  • types.ts: TreeNodeProvider.patchRagIndex?(ids, value): Promise<void> und canPatchRagIndex?(node): boolean ergaenzt.
  • types.ts: FormGeneratorTreeProps.refreshAfterAction?: boolean (default false, Backward-Compat fuer FilesTab).
  • FormGeneratorTree.tsx: dritter Flag-Button (_RAG_EMOJI, _ACTION_RAG) rendert genau dann, wenn node.ragIndexEnabled !== undefined. Mixed-Symbol identisch zu scope/neutralize.
  • _handleToggleRagIndex analog zu _handleToggleNeutralize -- mit gleichem Cascade-auf-Folder-Optimismus, der jedoch bei refreshAfterAction=true uebersprungen wird.
  • Sort-Comparator in _buildChildMap erweitert: bei Geschwistern mit displayOrder sortiert numerisch aufsteigend; Knoten ohne displayOrder kommen NACH Knoten mit. Vorrang vor folder-first.
  • _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).
  • _runAction ruft _refetchAllExpanded automatisch auf wenn refreshAfterAction=true. Spinner haelt durch bis Refetch fertig.
  • FilesTab: nicht angefasst. Default refreshAfterAction=false = bisheriger optimistic-update-Pfad.
  • 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

  • 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.
  • 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.
  • Alter _TreeNodeView/_FlagButton ad-hoc Renderer komplett entfernt.
  • SourcesTab.module.css wurde im Vorgaenger-Refactor schon entfernt -- keine Wiedereinfuehrung noetig.
  • 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

  • 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.
  • 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 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 platform-core/tests/unit/services/test_buildTree.py::TestSyntheticRoots DONE -- 7/7
T2 8 unit ja platform-core/tests/unit/services/test_buildTree.py::TestGetChildrenForParents (regress.) DONE -- 2/2
T3 8 unit ja platform-core/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 ui-nyla && npx tsc --noEmit --skipLibCheck && npm run build OPEN
  • 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): platform-core/modules/serviceCenter/services/serviceKnowledge/_inheritFlags.py, _buildTree.py
  • Backend-Routes (nicht antasten): platform-core/modules/routes/routeDataSources.py, platform-core/modules/features/workspace/routeFeatureWorkspace.py
  • Tree-Komponente: ui-nyla/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx
  • Referenz-Implementierung (nicht antasten): ui-nyla/src/components/UnifiedDataBar/FilesTab.tsx
  • Wegzuwerfender Code: ui-nyla/src/components/UnifiedDataBar/SourcesTab.tsx
  • PR: noch nicht erstellt.

Abschluss

  • b-reference/ui-nyla/formgenerator.md aktualisieren: Abschnitt "FormGeneratorTree" um die neuen Props (refreshAfterAction, node.ragIndexEnabled, provider.patchRagIndex) ergaenzen.
  • b-reference/ui-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.