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