fixes rag and workflow

This commit is contained in:
ValueOn AG 2026-05-19 16:47:48 +02:00
parent 9d636ace2a
commit 946ae4b8c9
8 changed files with 816 additions and 9 deletions

View file

@ -1,4 +1,4 @@
<!-- status: canonical -->
<!-- status: canonical -->
<!-- lastReviewed: 2026-05-12 -->
# Themen-Index für AI-Kontext
@ -51,6 +51,7 @@ Lade immer zuerst diese Datei. Dann gezielt die passende(n) Referenz-Datei(en).
| RAG Consent & Control | c-work/2-build/2026-05-rag-consent-and-control-implementation.md | Datenzentrierte Steuerung: `DataSource.ragIndexEnabled`, Walker-Refactor, Job-Cancel, UDB-Toggle, RagInventoryPage |
| UDB DataSource Settings (RAG-Limits) | c-work/2-build/2026-05-udb-datasource-settings.md | Settings-Icon ⚙️ pro Tree-Node, `DataSource.settings.ragLimits` als alleinige Quelle (kein Override-Layer), PATCH `/api/datasources/{id}/settings`, GET `/api/datasources/{id}/cost-estimate`, indikative USD-Schätzung |
| UDB Cascade-Inherit für DataSource-Flags | c-work/4-done/2026-05-udb-cascade-inherit.md | 3-wertige `neutralize`/`ragIndexEnabled`/`scope` (`null` = vererbt), `_inheritFlags.getEffectiveFlag()` (Path-Traversal), `cascadeResetDescendants()` setzt explizite Descendants-Werte beim Parent-Toggle auf `null`, Walker konsumieren pre-resolved Werte aus `_loadRagEnabledDataSources` |
| UDB Generic Tree Refactor (BE autoritativ, FE pure Renderer) | c-work/4-done/2026-05-udb-generic-tree-refactor.md | Single `POST /api/workspace/{id}/tree/children` mit `{parents}`-Liste liefert `nodesByParent` inkl. pre-computed `effectiveNeutralize/Scope/RagIndexEnabled` als `boolean\|'mixed'`. Orchestrator `serviceKnowledge/_buildTree.py`. SourcesTab.tsx auf ~530 Zeilen, kein optimistic update, Spinner pro pending Toggle, einheitliches Mixed-Symbol. FDS unterstuetzt nun RAG (gleiche 3-wertige Semantik wie DS). Geloescht: 7 alte Tree-Endpoints (`/resolve-flags`, `/connections`, `/connections/{id}/services`, `/connections/{id}/browse`, `/feature-connections`, `/feature-connections/{fiId}/tables`, `/feature-connections/{fiId}/parent-objects/{tableName}`). Bewusst weggelassen: FDS-Record-Expansion (Tabellen-Ebene reicht). |
| Zentrale Workflow-Admin (Meine Sicht) | c-work/1-plan/2026-04-automation-central-admin.md | `/automations` Tabs Dashboard + Workflows, `GET .../workflow-runs/workflows` |
| Web Image Search | c-work/1-plan/2026-03-web-image-search.md | WEB_SEARCH_MEDIA Feature |
| UI i18n / Sprachsets (done) | c-work/3-validate/2026-04-ui-i18n-dynamic-language-sets.md | Mehrsprachigkeit, `t()`, Sprachset-API, Admin-UI, AI-Übersetzung |

View file

@ -1,6 +1,6 @@
<!-- status: canonical -->
<!-- lastReviewed: 2026-05-15 -->
<!-- verifiedAgainst: frontend_nyla (codebase audit 2026-04-07, post Automation Unification) + Typed Action Architecture Phasen 1-5 + Vitest+RTL Setup + useVoiceStream STT open options (2026-05-10) + Teams Bot dashboard SSE + Module deep-link (2026-05-11) + RAG Consent & Control (RagInventoryPage, UDB 4. Toggle, RagRunningBadge, Wizard-Refactor) (2026-05-15) -->
<!-- verifiedAgainst: frontend_nyla (codebase audit 2026-04-07, post Automation Unification) + Typed Action Architecture Phasen 1-5 + Vitest+RTL Setup + useVoiceStream STT open options (2026-05-10) + Teams Bot dashboard SSE + Module deep-link (2026-05-11) + RAG Consent & Control (RagInventoryPage, UDB 4. Toggle, RagRunningBadge, Wizard-Refactor) (2026-05-15) + UDB Sources Generic-Tree Refactor (2026-05-18) -->
# Frontend Nyla -- Architektur
@ -31,9 +31,9 @@ Ergänzend typische Root-Dateien und Bereiche im Repo: `main.tsx`, `App.tsx`, `a
| Komponente | Zweck |
|------------|-------|
| `UnifiedDataBar (UDB)` | Multi-Tab-Panel: Chats, Files, Sources — wird in Workspace, CommCoach und Graphical Editor genutzt. Sources-Tab hat 4. Action-Button: RAG-Index-Toggle pro DataSource (Consent-gesteuert via `PATCH /api/rag/inventory/{dsId}/index-toggle`) |
| `UnifiedDataBar (UDB)` | Multi-Tab-Panel: Chats, Files, Sources — wird in Workspace, CommCoach und Graphical Editor genutzt. Sources-Tab rendert zwei `FormGeneratorTree`-Sektionen ("Persoenliche Quellen", "Mandanten-Daten") mit dedizierter UDB-Domain-Schicht (`components/UnifiedDataBar/sources/`); UDB-Provider mappen Backend-Tree-Nodes auf das generische `TreeNode`-Schema, populieren `extraActions` fuer RAG-Toggle + Settings, und triggern Sektion-Refresh nach Patches |
| `FormGenerator` | Dynamische Formulare/Tabellen aus Backend-Attribut-Definitionen (siehe [formgenerator.md](formgenerator.md)) |
| `FormGeneratorTree` | Generische Baumkomponente mit Provider-Pattern (`TreeNodeProvider`), Multiselect, DnD, Inline-Editing, Scope/Neutralize, Batch-Actions; ersetzt fruehere `FolderTree`-Komponente (siehe [formgenerator.md](formgenerator.md)) |
| `FormGeneratorTree` | Generische Baumkomponente mit Provider-Pattern (`TreeNodeProvider`), Multiselect, DnD, Inline-Editing, Scope/Neutralize, Batch-Actions, generische **Mixed-State**-Symbole, **Pending-Spinner** und **extraActions**-Slot. Konsumenten: `FilesTab`, `SourcesTab`, `FilesPage`. Siehe [formgenerator.md](formgenerator.md). |
| `MandateNavigation` | Feature-Baum-Navigation mit Mandanten-Kontext |
| `Automation2FlowEditor` | Konsolidierter n8n-style Flow-Builder (Graphical Editor) mit 3-Column-Layout: [UDB + Chat/Tracing] [Canvas + Header] [Nodes] |
| `FlowCanvas` | Custom React Canvas fuer Workflow-Graph-Rendering mit Node-Highlighting (SSE Live-Tracing) |

View file

@ -399,10 +399,51 @@ Alle Mutations-Methoden sind optional -- ein read-only Provider implementiert nu
- **DnD** (`application/x-poweron-tree-items`); aus `shared` nur lesende Drops
- **Inline-Rename** (Doppelklick / F2) -- nur fuer eigene Knoten
- **Scope/Neutralize** Icons: interaktiv fuer eigene, Indikator fuer geteilte
- **Mixed-State** (generisch): `scope`, `neutralize` und beliebige `extraActions[*].value`
duerfen den Wert `'mixed'` haben (vom Provider/Backend gesetzt). Der Tree rendert
dann ein uniformes Symbol (`\u25E9`) statt des typspezifischen Icons.
- **Pending-Spinner** (generisch): waehrend einer asynchronen Provider-Aktion
(`patchScope`, `patchNeutralize`, oder `extraActions[*].onClick`) zeigt der Tree
einen kleinen rotierenden Spinner ueber dem jeweiligen Button.
- **extraActions** (generisch): Knoten koennen eine Liste zusaetzlicher Aktionen
mitbringen (`TreeNode.extraActions: NodeAction[]`). Der Tree rendert sie als
Icon-Buttons mit Tooltip; er kennt deren Semantik nicht und reicht nur Klicks
in den Pending-Spinner-Wrapper.
- **Batch-Actions** mit `typeFilter` (z.B. separate Delete-Buttons fuer Ordner vs. Dateien)
- **Refresh-Button** im Section-Header
- **Confirmation-Dialoge** vor Delete-Aktionen
### Generische Erweiterungen (Mixed, Pending, extraActions)
```typescript
interface NodeAction {
key: string; // eindeutig pro Node
icon: React.ReactNode; // Standard-Icon (durch mixed/spinner ueberschrieben)
tooltip: string;
value?: boolean | string | 'mixed';
disabled?: boolean;
onClick?: () => Promise<void> | void;
}
interface TreeNode<T> {
// ...
scope?: ScopeValue | 'mixed';
neutralize?: boolean | 'mixed';
extraActions?: NodeAction[];
}
```
Rendering-Regeln:
| Zustand | Anzeige |
|---------|--------|
| `value !== 'mixed'`, kein Pending | typspezifisches Icon (Scope-Emoji, Schloss, Action-Icon) |
| `value === 'mixed'` | `\u25E9` mit Klasse `flagMixed` (uniform fuer alle Flags) |
| Pending (Promise laeuft) | rotierender Spinner mit Klasse `flagSpinner` |
Diese Features sind domaen-frei und werden von beiden FormGeneratorTree-Konsumenten
(`FilesTab.tsx` und `UnifiedDataBar/SourcesTab.tsx`) genutzt.
### Props (`FormGeneratorTreeProps`)
| Prop | Typ | Beschreibung |
@ -413,6 +454,7 @@ Alle Mutations-Methoden sind optional -- ein read-only Provider implementiert nu
| `compact` | `boolean?` | Kompakte Darstellung (z.B. in UDB) |
| `collapsible` | `boolean?` | Section kollabierbar |
| `defaultCollapsed` | `boolean?` | Initial kollabiert |
| `selectable` | `boolean?` | Default `true`. Bei `false` keine Checkboxes, kein Batch-Toolbar (UDB-Sources-Modus) |
| `emptyMessage` | `string?` | Anzeige bei leerem Tree |
| `onNodeClick` | `(node) => void` | Callback bei Knoten-Klick |
| `onSelectionChange` | `(selectedIds) => void` | Callback bei Selektions-Aenderung |
@ -420,9 +462,11 @@ Alle Mutations-Methoden sind optional -- ein read-only Provider implementiert nu
### Verwendung
Aktuell in zwei Kontexten eingebunden, jeweils mit `FolderFileProvider`:
Aktuell in drei Kontexten eingebunden, jeweils mit eigenem Provider:
- **`UnifiedDataBar/FilesTab.tsx`**: Zwei Sektionen (Eigene / Geteilt mit mir)
- **`UnifiedDataBar/FilesTab.tsx`**: Zwei Sektionen (Eigene / Geteilt mit mir), Provider: `FolderFileProvider`
- **`UnifiedDataBar/SourcesTab.tsx`**: Zwei Sektionen (Persoenliche Quellen / Mandanten-Daten),
Provider: `createUdbPersonalProvider` + `createUdbFeatureProvider` aus `components/UnifiedDataBar/sources/`
- **`pages/basedata/FilesPage.tsx`**: Split-View mit Tree links und `FormGeneratorTable` rechts;
Tree-Selektion filtert Tabelle nach Ordner

View file

@ -1,6 +1,6 @@
<!-- status: canonical -->
<!-- lastReviewed: 2026-05-18 -->
<!-- verifiedAgainst: gateway (codebase audit 2026-04-16, RAG Consent & Control update 2026-05-15, Cascade-Inherit update 2026-05-18) -->
<!-- verifiedAgainst: gateway (codebase audit 2026-04-16, RAG Consent & Control update 2026-05-15, Cascade-Inherit + Generic Tree Refactor update 2026-05-18) -->
# Neutralisierungs-System
@ -47,7 +47,7 @@ Im zentralen AI-Gate (`_neutralizeRequest`):
1. **Toggle `FileItem.neutralize`:** Index und Chunks werden synchron gelöscht; Re-Index im Hintergrund verhindert Leaks bei Fehlern in der Nachindexierung.
2. **Toggle `FileFolder.neutralize`:** Propagiert `neutralize` rekursiv auf alle Dateien im Ordner; Index/Chunks werden pro Datei synchron gelöscht, Re-Index im Hintergrund. Neue/verschobene Dateien erben das Flag des Ziel-Ordners.
3. **Indexierung:** `indexFile()` neutralisiert Text-Chunks bei `FileItem.neutralize=True` → Data-at-rest abgesichert.
4. **DataSource-Download / RAG-Indexierung:** `DataSource.neutralize` ist die einzige Quelle für die Neutralisierungs-Policy pro Tree-Element. **Seit 2026-05 (Cascade-Inherit):** `neutralize`, `ragIndexEnabled` und `scope` sind 3-wertig (`null` = vererbt, `True/False` bzw. ein Scope-String = explizit). `null` in der Datenbank bedeutet: Wert vom nächsten Vorfahren-DataSource im Path-Tree übernehmen (longest-prefix wins, gleicher `connectionId` + `sourceType`). Beim Setzen eines expliziten Werts auf einem Parent **kaskadiert das Backend** (`cascadeResetDescendants`) durch alle Descendants und setzt deren explizite Werte für **dieses eine Flag** auf `null` zurück — Descendants die bereits geerbt haben, werden nicht angefasst. Walker konsumieren pre-resolved Werte: `_loadRagEnabledDataSources` (in `subConnectorIngestConsumer.py`) berechnet pro DS via `_inheritFlags.getEffectiveFlag()` den effektiven `ragIndexEnabled`/`neutralize`/`scope` und schreibt das in das Walker-Input-Dict — die Sync-Walker (`subConnectorSync*.py`) bleiben dadurch unverändert und lesen weiter direkt `ds.get("neutralize", False)`. Die Flag-Werte werden zusätzlich auf erzeugte `FileItem`s übernommen. Der frühere `subPolicyResolver` ist auf einen Backward-Compat-Shim auf `getEffectiveFlag` reduziert.
4. **DataSource-Download / RAG-Indexierung:** `DataSource.neutralize` ist die einzige Quelle für die Neutralisierungs-Policy pro Tree-Element. **Seit 2026-05 (Cascade-Inherit):** `neutralize`, `ragIndexEnabled` und `scope` sind 3-wertig (`null` = vererbt, `True/False` bzw. ein Scope-String = explizit). `null` in der Datenbank bedeutet: Wert vom nächsten Vorfahren-DataSource im Path-Tree übernehmen (longest-prefix wins, gleicher `connectionId` + `sourceType`). Beim Setzen eines expliziten Werts auf einem Parent **kaskadiert das Backend** (`cascadeResetDescendants`) durch alle Descendants und setzt deren explizite Werte für **dieses eine Flag** auf `null` zurück — Descendants die bereits geerbt haben, werden nicht angefasst. Walker konsumieren pre-resolved Werte: `_loadRagEnabledDataSources` (in `subConnectorIngestConsumer.py`) berechnet pro DS via `_inheritFlags.getEffectiveFlag()` den effektiven `ragIndexEnabled`/`neutralize`/`scope` und schreibt das in das Walker-Input-Dict — die Sync-Walker (`subConnectorSync*.py`) bleiben dadurch unverändert und lesen weiter direkt `ds.get("neutralize", False)`. Die Flag-Werte werden zusätzlich auf erzeugte `FileItem`s übernommen. Der frühere `subPolicyResolver` ist auf einen Backward-Compat-Shim auf `getEffectiveFlag` reduziert. **Seit 2026-05-18 (Generic Tree Refactor):** `FeatureDataSource` traegt nun ebenfalls `ragIndexEnabled` mit gleicher 3-wertiger Semantik (`_INHERITABLE_FDS_FLAGS` enthaelt alle drei Flags); `PATCH /api/datasources/{id}/rag-index` akzeptiert via `_findSourceRecord` sowohl DS als auch FDS — Bootstrap-Job und Chunk-Purge bleiben aber DS-only (FDS-RAG ist Feature-Pipeline-getrieben, das Flag steuert ausschliesslich die Pipeline-Aktivierung).
5. **FeatureDataSource (Tabellen-Level):** `_queryFeatureInstance()` setzt bei `neutralize=True` u. a. `requireNeutralization=True` für Sub-Agent-AI-Calls.
6. **FeatureDataSource (Feld-Level):** `neutralizeFields` definiert Spalten, deren Werte in `FeatureDataProvider` mit deterministischen Platzhaltern `[NEUT.<field>.<hash>]` ersetzt werden, bevor Daten den Sub-Agent erreichen. Gleichheits-Vergleiche bleiben möglich (stabiler Hash).
7. **AI-Call:** Bei erzwungener Neutralisierung (`requireNeutralization=True`) schlägt fehlgeschlagene Neutralisierung hart fehl (`RuntimeError` / Blockierung bzw. Entfernen betroffener Message-Teile — kein Durchleiten im Rohzustand).
@ -55,6 +55,15 @@ Im zentralen AI-Gate (`_neutralizeRequest`):
**Engine-Failsafe (Spezifikation):** Neutralisierung verlangt, Engine nicht verfügbar → nicht weiterverarbeiten; Teilfehler → Teil entfernen, nicht roh weitergeben; fehlendes internes Medienmodell → Medien-Part entfernen.
## Unified Data Bar — Tree-Endpoint und Flag-Toggling
Seit 2026-05-18 (Generic Tree Refactor, siehe `c-work/4-done/2026-05-udb-generic-tree-refactor.md`) liefert ein einziger Endpoint die komplette sichtbare Tree-Struktur des UDB inklusive aller drei effektiver Flag-Werte:
- **Endpoint:** `POST /api/workspace/{instanceId}/tree/children` mit Body `{parents: [null, "<key1>", "<key2>", ...]}`.
- **Response:** `{nodesByParent: {"__root__": [...], "<key1>": [...], ...}}` — jeder `TreeNode` enthält `effectiveNeutralize`, `effectiveScope`, `effectiveRagIndexEnabled` als `boolean | "mixed"` bzw. `string | "mixed"` (mode=`aggregate`).
- **Builder:** `gateway/modules/serviceCenter/services/serviceKnowledge/_buildTree.py` orchestriert alle Kinds (`connection`, `service`, `folder`, `file`, `mandateGroup`, `featureNode`, `fdsTable`) und ruft `resolveEffectiveForPath` / `resolveEffectiveForFds` pro Node — auch fuer Coordinates ohne eigenen DB-Record (virtueller Datensatz mit `null`-Flags).
- **Frontend:** `SourcesTab.tsx` rendert ausschliesslich die Backend-Antwort, keine Vererbungslogik. Toggle = PATCH -> Refetch -> Render mit Spinner ueber dem Flag-Button waehrend des Round-Trips. Mixed-Symbol `U+25E9` einheitlich fuer alle drei Flags.
## NeutralizationPanel (Frontend)
`frontend_nyla/src/pages/views/workspace/NeutralizationPanel.tsx` — Übersicht zu Dateien mit `neutralize`-Flag, Status und Platzhalter-Mappings. Datenbasis u. a. `GET /api/workspace/{instanceId}/files``{ files: [...] }`.
@ -73,7 +82,9 @@ Im zentralen AI-Gate (`_neutralizeRequest`):
| **Ordner-Toggle / Index** | `routes/routeDataFiles.py``PATCH /folders/{folderId}/neutralize` (rekursive Propagierung) |
| **Workflow-Aktion** | `workflows/methods/methodContext/actions/neutralizeData.py` |
| **Kontext** | `serviceCenter/context.py``requireNeutralization` |
| **Flags (Modelle)** | `datamodels/datamodelFiles.py` (`FileItem.neutralize`), `datamodelFileFolder.py` (`FileFolder.neutralize`), `datamodelDataSource.py`, `datamodelFeatureDataSource.py` (`neutralize`, `neutralizeFields`) |
| **Flags (Modelle)** | `datamodels/datamodelFiles.py` (`FileItem.neutralize`), `datamodelFileFolder.py` (`FileFolder.neutralize`), `datamodelDataSource.py`, `datamodelFeatureDataSource.py` (`neutralize`, `neutralizeFields`, `ragIndexEnabled` seit 2026-05-18) |
| **Effective-Flag-Resolution** | `serviceCenter/services/serviceKnowledge/_inheritFlags.py` (`getEffectiveFlag`, `getEffectiveFlagFds`, `resolveEffectiveForPath`, `resolveEffectiveForFds`, `cascadeResetDescendants`) |
| **UDB Tree-Builder** | `serviceCenter/services/serviceKnowledge/_buildTree.py` (`getChildrenForParents` -> `POST /api/workspace/{id}/tree/children`) |
| **AI-Operationen / Enums** | `datamodels/datamodelAi.py`, `aicore/aicorePluginPrivateLlm.py` |
## Regeln / Invarianten

View file

@ -0,0 +1,387 @@
<!-- 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.

View file

@ -0,0 +1,187 @@
<!-- status: build -->
<!-- started: 2026-05-17 -->
<!-- completed: 2026-05-17 -->
<!-- component: gateway | frontend-nyla -->
# UDB DataSource Settings (⚙️) + konfigurierbare RAG-Limits
> **Implementierungsstand 2026-05-17:** S0S11 abgeschlossen. Wichtige Abweichung
> vom ursprünglichen Plan: das "lazy-fill defaults beim ersten Bootstrap"
> wurde **nicht** im Walker implementiert (würde die Caller-Override-Semantik
> aushebeln und liess Tests rot werden). Stattdessen: `_ragLimits` exponiert
> zwei klare APIs — `getStoredOverrides()` (nur explizite Overrides, für Walker)
> und `getRagLimits()` (Overrides + Defaults gemerged, für API/Cost-Estimate).
> Das Modal zeigt fehlende Werte als Placeholder aus dem Cost-Estimate-Endpoint
> an, damit der User immer eine konkrete Zahl sieht — ohne die Datenbank-Zeile
> dafür anfassen zu müssen. Siehe CHANGELOG 2026-05-17 für die Detail-Liste.
## Beschreibung und Kontext
Heute sind die RAG-Walker-Limits (`maxBytes`, `maxItems`, `maxFileSize`, `maxDepth`, `maxWorkspaces`, `maxListsPerWorkspace`, `maxTasks`) als Modul-Konstanten in den `subConnectorSync*.py`-Dateien hartkodiert. Ein User mit z. B. einem 500 MB grossen SharePoint-Folder erhält nach 200 MB ein stilles Stoppen (`stoppedAtLimit=maxBytes`) und kann das ohne Code-Änderung nicht anpassen. Heute (2026-05-17) sichtbar geworden auf einem Basecamp-Folder mit ~731 Dateien, von denen nur 25 indexiert wurden, bevor der `maxBytes`-Default zugriff.
Gleichzeitig fehlt für Feature-Instanz-DataSources (`FeatureDataSource`) bisher überhaupt ein UI, an dem der Owner Settings anpassen könnte. UserConnections haben den Wizard und das Edit-Modal auf der Connections-Seite für Feature-Daten gibt es keinen analogen Ort.
Beide Lücken lassen sich mit derselben Architektur schliessen: **ein generisches Settings-Icon (⚙️) pro Node in der UDB**, das ein typ- und scope-abhängiges Modal öffnet. Initial wird darin RAG-Limit-Werte und Connection-Master-Switch gepflegt, später kommen weitere Einstellungen (z. B. Custom-Neutralization-Regeln, Re-Sync-Cadence, Polling-Strategie) ohne neue Icons dazu.
Wichtiger Architektur-Constraint:
- **Keine Icon-Inflation in der UDB.** Bereits vorhanden: Scope-Toggle, Neutralize-Toggle, RAG-Index-Toggle (🧠), Chat (💬). Wir fügen genau **ein** zusätzliches Icon (⚙️) hinzu nicht ein Icon pro Setting.
## Fokus und kritische Details
- UDB ist die **einzige** Stelle, wo Settings pro Datenobjekt gemanagt werden. Andere Pfade (Wizard, Connection-Edit-Modal) editieren nur die Connection-Ebene, nie einzelne DataSources.
- **Speicherort = Quelle der Wahrheit. Keine Override-Schichten, keine Resolver-Logik.** Was im UI angezeigt wird, ist exakt das, was im Walker greift. Die Werte werden bei der DataSource-Erstellung mit sinnvollen Defaults vorbefüllt (kopiert aus globalen Konstanten); danach kann der User sie einfach editieren. Vorteil: nachvollziehbar, debuggbar, keine versteckten Vererbungen.
- Walker müssen ihre Default-Konstanten zentralisieren und beim Lesen einer DataSource entweder den dort gespeicherten Wert nehmen oder, falls noch nicht gesetzt, den zentralen Default ausliefern und gleichzeitig auf der DataSource persistieren (lazy initialization → ab dann sind die Werte sichtbar im UI).
- Settings-Werte landen in einer **JSON-Spalte** auf der DataSource bzw. FeatureDataSource (`settings: Optional[Dict[str, Any]]`) damit ist die Erweiterbarkeit garantiert, ohne neue Spalten/Migrationen pro neuem Setting.
- DataSource vs. FeatureDataSource: gleiche Spalte (`settings`), gleiches UI, gleiches API-Pattern (`PATCH /api/datasources/{id}/settings` und `PATCH /api/feature-data-sources/{id}/settings`).
- Audit-Log-Pflicht: jede Änderung an `settings` schreibt einen `AuditCategory.PERMISSION`-Eintrag (auch wenn es nur ein numerischer Limit-Wert ist RAG-Ingest-Grenze ist eine Compliance-relevante Entscheidung).
- Defaults bleiben **konservativ** (200 MB / 10'000 Items / 25 MB / Tiefe 8). Anheben braucht User-Aktion → keine versehentlichen Kostenexplosionen.
- **Connection-Master-Switch im Modal:** Im Settings-Modal einer DataSource ist auch der Connection-weite Master-Switch `knowledgeIngestionEnabled` ("Knowledge database active") togglebar damit ist das Modal die zentrale Stelle für alle relevanten Settings, ohne zwischen Seiten wechseln zu müssen. Dieser Toggle wirkt auf alle DataSources derselben Connection (es ist explizit als "Connection-Setting" gekennzeichnet, nicht als DataSource-Setting).
- **Kosten-Indikator statt Hard-Cap:** Keine harten Obergrenzen. Stattdessen zeigt das Modal eine indikative (nicht-verbindliche) Realtime-Kostenschätzung: basierend auf erwarteter Item-Zahl, Token-Schätzung pro Item und aktuellen Embedding-Preisen. Klar als "indikativ" gekennzeichnet, der User trägt die Verantwortung.
## Ziel und Nicht-Ziele
- **Ziel:**
- Ein Settings-Icon ⚙️ pro Tree-Node in der UDB-Sidebar (`SourcesTab.tsx`), das ein Modal öffnet.
- Im Modal: drei klar abgegrenzte Sektionen:
1. **Connection** (Master-Switch `knowledgeIngestionEnabled`, gemeinsame Mail-/ClickUp-Preferences aus `knowledgePreferences`).
2. **DataSource RAG-Limits** (`maxBytes`, `maxFileSize`, `maxItems`, `maxDepth`).
3. **Kostenschätzung** (indikativer Wert mit Hinweis).
- Backend: `DataSource.settings` und `FeatureDataSource.settings` als JSON-Spalte. Endpunkte zum Lesen/Patchen.
- Walker (`subConnectorSyncSharepoint.py`, `subConnectorSyncKdrive.py`, `subConnectorSyncGdrive.py`, `subConnectorSyncClickup.py`) lesen Limits direkt aus `DataSource.settings.ragLimits`, fallen auf zentrale Defaults zurück, wenn (noch) leer.
- Partial-Banner auf `RagInventoryPage` zeigt zusätzlich Hint: "Limit kann pro DataSource in der UDB unter ⚙️ angehoben werden."
- Owner-Kontrolle: für UserConnection-DataSources nur Owner; für FeatureDataSource Owner oder `workspace-admin`.
- **Nicht-Ziel:**
- Mandate-weite Defaults / Override-Schichten / Resolver-Layer.
- Hard-Caps (User/Admin trägt Verantwortung).
- Settings-Vererbung im Tree (Parent-Folder → Children) aktuell wirkt eine Änderung nur auf die konkrete DataSource.
- Eigene Settings für Mail-Connectors (`Outlook`, `Gmail`) auf DataSource-Ebene die haben keine Folder-Hierarchie. Die Mail-Preferences bleiben Connection-weit und werden im Modal in der "Connection"-Sektion editiert.
## Architektur-Skizze
### Daten-Schicht
`DataSource` und `FeatureDataSource` bekommen je eine neue Spalte:
```python
settings: Optional[Dict[str, Any]] = Field(
default=None,
description="DataSource-scoped settings (JSON). Currently used keys: ragLimits.",
json_schema_extra={"frontend_type": "json", "frontend_readonly": True, "frontend_required": False},
)
```
JSON-Schema-Konvention:
```json
{
"ragLimits": {
"maxBytes": 524288000,
"maxFileSize": 52428800,
"maxItems": 20000,
"maxDepth": 12
}
}
```
**Keine Resolver-Schichten**: was in `settings.ragLimits` steht, gilt. Wenn der Key fehlt, nimmt der Walker den zentralen Default aus `_ragLimits.RAG_LIMITS_DEFAULT` und schreibt ihn beim nächsten Bootstrap **einmalig** in die DataSource zurück (lazy fill), damit der User die Werte auch ohne vorherigen Sync schon im UI sieht und editieren kann.
### Backend-API
```
PATCH /api/datasources/{id}/settings { settings: { ragLimits: {...} } }
PATCH /api/feature-data-sources/{id}/settings { settings: { ragLimits: {...} } }
GET /api/datasources/{id}/cost-estimate → { estimatedTokens, estimatedUsd, basis: {...} }
```
`/cost-estimate`: schätzt anhand Item-Count (sofern bekannt aus letztem Sync) × tokens-per-item-heuristik × Embedding-Preis. Liefert auch die Annahmen (`basis`), damit der User die Plausibilität prüfen kann.
### Walker-Refactor
Heute hartkodiert in jedem Walker:
```python
MAX_BYTES_DEFAULT = 200 * 1024 * 1024
MAX_ITEMS_DEFAULT = 10_000
```
Wird zu:
```python
# gateway/modules/serviceCenter/services/serviceKnowledge/_ragLimits.py
RAG_LIMITS_DEFAULT = {
"maxBytes": 200 * 1024 * 1024,
"maxFileSize": 25 * 1024 * 1024,
"maxItems": 10_000,
"maxDepth": 8,
}
def getRagLimits(dataSource: Dict[str, Any]) -> Dict[str, int]:
"""Read limits from DataSource.settings.ragLimits, fall back to defaults
for missing keys. Pure read; lazy persist is the caller's responsibility."""
stored = (dataSource.get("settings") or {}).get("ragLimits") or {}
return {**RAG_LIMITS_DEFAULT, **stored}
def lazyFillRagLimits(rootIf, dataSourceId: str, dataSource: Dict[str, Any]) -> None:
"""Persist defaults to settings if not yet present, so the UI shows them."""
```
Jeder Walker holt seine Limits beim Eintritt in `_bootstrap*` einmal und gibt sie an `_walkFolder()` / `_finalizeResult()` weiter die Konstante existiert nur noch im neuen Modul als Default-Fallback.
### Frontend-UI
In `SourcesTab.tsx` (Tree-Node-Render), zwischen Brain-Icon (🧠) und Chat-Icon (💬):
```typescript
<button
onClick={async (e) => {
e.stopPropagation();
const dsId = ds?.id ?? await onEnsureDs(node);
if (dsId) openSettingsModal(dsId, scope);
}}
title={t('Einstellungen')}
style={{ ... }}
>
⚙️
</button>
```
Settings-Modal (`DataSourceSettingsModal.tsx`):
- **Sektion "Connection"** (oben, mit Connection-Label und Authority-Icon):
- Toggle `knowledgeIngestionEnabled` (Master-Switch). Hinweis: wirkt auf alle DataSources dieser Connection.
- Optional, wenn relevant: `mailContentDepth`, `mailIndexAttachments`, `filesIndexBinaries`, `clickupScope`, `clickupIndexAttachments`, `maxAgeDays` (aus `knowledgePreferences`).
- PATCH → `/api/connections/{id}/knowledge-consent` bzw. `/api/connections/{id}/knowledge-preferences`.
- **Sektion "RAG-Limits"** (nur wenn DataSource den Walker-Typ unterstützt):
- Felder `maxBytes`, `maxFileSize`, `maxItems`, `maxDepth` mit lesbaren Units (MB, Anzahl).
- PATCH → `/api/datasources/{id}/settings`.
- **Sektion "Kostenschätzung"** (read-only):
- "Indikative Kosten: ~X USD pro Voll-Sync" (mit Basis-Tooltip).
- GET → `/api/datasources/{id}/cost-estimate` beim Modal-Öffnen.
Auf der `RagInventoryPage`-Partial-Banner-Komponente:
```typescript
<span>... Limit {l} erreicht. Weitere Dateien wurden NICHT indexiert. {' '}
<a onClick={() => openSettingsModalForConn(conn)}>Limit anpassen</a> oder
DataSource enger eingrenzen, dann erneut starten.</span>
```
## Erfolgskriterien
- Auf einer SharePoint-DataSource mit > 200 MB kann der Owner über das ⚙️-Icon `maxBytes` auf z. B. 1 GB anheben, einen Re-Index starten und sieht im Inventory > 200 MB indexierte Bytes ohne `stoppedAtLimit`-Banner.
- Auf einer FeatureDataSource ist das gleiche Settings-Modal verfügbar und speichert sauber in `FeatureDataSource.settings`.
- Wenn `settings.ragLimits` leer ist, ist das Verhalten der Walker **bitidentisch** zur Vor-Plan-Version (keine Regression).
- Der Connection-Master-Switch im Modal und der Toggle auf der Connections-Page und auf der RagInventoryPage zeigen immer denselben Wert (alle drei rufen `/knowledge-consent`).
- Audit-Log enthält pro Settings-Change einen `PERMISSION`-Eintrag mit `dataSourceId`, `userId`, `mandateId`, `oldSettings`, `newSettings`.
- Keine zusätzlichen Icons in der UDB ausser dem einen ⚙️.
- Kostenschätzung wird als "indikativ, nicht verbindlich" gekennzeichnet und zeigt die zugrunde liegenden Annahmen.
## Schritte
- [ ] S1 — Datenmodell: `settings: JSON` an `DataSource` und `FeatureDataSource` ergänzen (`gateway/modules/datamodels/datamodelDataSource.py`, `datamodelFeatureDataSource.py`); SQL-Migration in `gateway/scripts/script_db_migrate_datasource_settings.py`.
- [ ] S2 — Zentralisierte Defaults: `gateway/modules/serviceCenter/services/serviceKnowledge/_ragLimits.py` neu mit `RAG_LIMITS_DEFAULT`, `getRagLimits()`, `lazyFillRagLimits()`.
- [ ] S3 — Walker-Refactor: `subConnectorSyncSharepoint.py`, `subConnectorSyncKdrive.py`, `subConnectorSyncGdrive.py`, `subConnectorSyncClickup.py` lesen Limits via `getRagLimits()` und schreiben Defaults via `lazyFillRagLimits()` zurück.
- [ ] S4 — Backend-Endpunkte: `PATCH /api/datasources/{id}/settings`, `PATCH /api/feature-data-sources/{id}/settings`, `GET /api/datasources/{id}/cost-estimate` in `routeDataSources.py` (und `routeFeatureDataSources.py`, falls noch nicht da). RBAC: Owner für DataSource, Owner + `workspace-admin` für FeatureDataSource.
- [ ] S5 — Frontend ⚙️-Button in `SourcesTab.tsx` (Tree-Row, zwischen 🧠 und 💬), Opacity-Logik: voll sichtbar, wenn `settings` befüllt sind, sonst gedimmt.
- [ ] S6 — Frontend `DataSourceSettingsModal.tsx`: drei Sektionen (Connection / RAG-Limits / Kostenschätzung), per-Feld-Validation, Speichern via API.
- [ ] S7 — `RagInventoryPage.tsx` Partial-Banner: Link "Limit anpassen" öffnet Modal für die betroffene DataSource (Heuristik: DS, die `stoppedAtLimit` ausgelöst hat bei mehreren: die mit den meisten verarbeiteten Bytes).
- [ ] S8 — Audit-Logging in den neuen Settings-Endpunkten.
- [ ] S9 — Cost-Estimate-Engine: `gateway/modules/serviceCenter/services/serviceKnowledge/_costEstimate.py` mit Heuristik (Items × Tokens × Embedding-Preis) und Basis-Annahmen-Output.
- [ ] S10 — Tests: Unit-Tests für `getRagLimits()` / `lazyFillRagLimits()`, API-Tests für PATCH-Endpunkte (RBAC), Cost-Estimate-Unit-Test, Frontend-Smoke-Test (Modal öffnet, speichert, refetch, Cost-Anzeige).
- [ ] S11 — Doku: `wiki/b-reference/platform/rag-pipeline.md` Abschnitt "Limits & Settings" ergänzen, plus Eintrag in `wiki/TOPICS.md`. CHANGELOG-Zeile.
## Offene Fragen / Decisions
- **F1 Override-Logik?****Entschieden: KEINE Override-Schichten.** Die DataSource speichert ihre eigenen Werte direkt; Walker liest sie 1:1. Lazy-Fill mit zentralen Defaults beim ersten Sync, damit das UI immer einen sinnvollen Startwert zeigt. Nachvollziehbar, debuggbar, keine versteckten Vererbungen.
- **F2 RBAC?****Entschieden:** Owner für UserConnection-DataSources, Owner oder `workspace-admin` für FeatureDataSource (analog Scope-/Neutralize-Toggle).
- **F3 Hard-Cap pro Mandat?****Entschieden: NEIN.** Stattdessen indikative Realtime-Kostenschätzung im Modal. User/Admin trägt Verantwortung.
- **F4 Mail/ClickUp-Preferences im Modal?****Entschieden: JA.** Connection-weite Preferences werden in der Sektion "Connection" des Modals editiert das macht das Modal zur einzigen Stelle für alle relevanten Settings, ohne Seitenwechsel.

View file

@ -0,0 +1,158 @@
<!-- status: done -->
<!-- started: 2026-05-18 -->
<!-- completed: 2026-05-18 -->
<!-- component: gateway | frontend-nyla -->
<!-- lastReviewed: 2026-05-18 -->
<!-- verifiedAgainst: gateway/modules/serviceCenter/services/serviceKnowledge/_buildTree.py | gateway/modules/serviceCenter/services/serviceKnowledge/_inheritFlags.py | gateway/modules/features/workspace/routeFeatureWorkspace.py | gateway/modules/routes/routeDataSources.py | gateway/modules/datamodels/datamodelFeatureDataSource.py | frontend_nyla/src/components/UnifiedDataBar/SourcesTab.tsx -->
# UDB Generic Tree Refactor (Backend authoritativ, FE pure Renderer)
## Kontext
Die `SourcesTab.tsx` des Unified Data Bars trug ein wachsendes Konsistenzproblem: Toggles auf Parent-Nodes hatten unter bestimmten Bedingungen keinen UI-Effekt. Wiederholte Bugfix-Iterationen am gleichen Datum (siehe `2026-05-udb-cascade-inherit.md`) haben drei Symptome verschoben statt geheilt, weil im Frontend parallele Logik (`_resolveFdsFlags`, optimistic Updates, `_findCoveringDs`) den vom Backend gelieferten Werten widerspricht.
User-Direktive: Das Backend ist die einzige Quelle der Wahrheit fuer die effektiven Flag-Werte aller sichtbaren Tree-Nodes. Das Frontend rendert nur. Keine Vererbung im Frontend. Keine optimistischen Updates.
## Ziele
1. Ein einziger Endpoint liefert auf Anfrage Children-Listen pro Parent-Key, inklusive aller drei effektiven Flag-Werte (`neutralize`, `scope`, `ragIndexEnabled`) als `boolean | "mixed"` bzw. `string | "mixed"`.
2. Das Frontend halt nur einen `Map<parentKey, TreeNode[]>` und ein `Set<expandedKeys>`. Re-Render = Re-Fetch.
3. Toggle-Flow strikt: PATCH -> Refetch -> Render. Pro Klick wird der entsprechende Flag-Button als Spinner gezeigt, bis die neuen Daten gerendert sind.
4. Ein einziges, von allen anderen Symbolen klar unterscheidbares `mixed`-Symbol gilt fuer alle drei Flags.
5. RAG-Indexierung ist auf der gleichen Stufenmechanik fuer FDS verfuegbar wie fuer Personal-DataSources.
## Architektur
### Tree-Endpoint
```
POST /api/workspace/{instanceId}/tree/children
Body: { "parents": [null, "conn|<id>", "feat|<mid>|<code>|<fiId>", ...] }
Response: { "nodesByParent": { "__root__": [...], "conn|<id>": [...], ... } }
```
`null` markiert die Top-Level-Ebene. Pro Parent-Key kommt eine Liste von `TreeNode`-Dicts zurueck.
`TreeNode` ist die einzige Schema-Struktur fuer alle Kinds:
```
key, kind, parentKey, label, icon, hasChildren,
dataSourceId, modelType,
effectiveNeutralize, effectiveScope, effectiveRagIndexEnabled,
supportsRag, canBeAdded,
+ kind-spezifische Carrier (authority, connectionId, service, sourceType, path,
featureInstanceId, featureCode, mandateId, tableName, objectKey)
```
Mit `kind in {connection, service, folder, file, mandateGroup, featureNode, fdsTable, fdsRecord}`. Der `fdsRecord`-Kind ist im Schema vorgesehen, aber im aktuellen Builder nicht aktiviert (Records werden bewusst nicht expandierbar gerendert, siehe "Bewusste Reduzierung" unten).
### Key-Format
Stable, parsebar, kollisionsfrei. Pipe-Separator:
| Pattern | Bedeutung |
|---|---|
| `conn\|<connId>` | UserConnection-Root |
| `svc\|<connId>\|<service>` | Service unter Connection (sharepoint, outlook, ...) |
| `ds\|<connId>\|<sourceType>\|<path>` | Folder oder File innerhalb eines Services |
| `mgrp\|<mandateId>` | Mandanten-Gruppen-Kopfknoten |
| `feat\|<mandateId>\|<featureCode>\|<fiId>` | Feature-Instanz (Workspace-Wildcard `*`) |
| `fdstbl\|<fiId>\|<tableName>` | Feature-Datentabelle |
| `fdsrec\|<fiId>\|<tableName>\|<recordId>` | (reserviert, derzeit nicht emittiert) |
### Builder
`gateway/modules/serviceCenter/services/serviceKnowledge/_buildTree.py` orchestriert pro Request:
- Vor-Laden aller DataSource-Records des Users (`recordFilter={"userId": userId}`) und aller FeatureDataSource-Records des Workspaces (`recordFilter={"workspaceInstanceId": instanceId}`) einmal.
- Dispatch pro Parent-Kind:
- `null` -> aktive Connections + zugaengliche Mandanten-Gruppen
- `conn|*` -> verfuegbare Services (`provider.getAvailableServices()`)
- `svc|*` / `ds|*` -> `adapter.browse(path)` via ConnectorResolver
- `mgrp|*` -> per-Mandate Feature-Instanzen mit `featureCode in featuresWithDataObjects`
- `feat|*` -> `catalog.getDataObjects()` gefiltert via RBAC
- Pro Node werden die drei `effective*`-Werte ueber `resolveEffectiveForPath` / `resolveEffectiveForFds` (mode=`aggregate`) berechnet. Diese Helpers funktionieren auch fuer Coordinates ohne eigenen DB-Record (virtueller Datensatz mit `null`-Flags).
- Sourcetype-Mapping (`sharepoint -> sharepointFolder`, `onedrive -> onedriveFolder`, ...) lebt nun ausschliesslich im Builder, nicht mehr im Frontend.
### Frontend (`SourcesTab.tsx`)
`~530` Zeilen, ersetzt die alte `~2500`-Zeilen-Version komplett.
```mermaid
sequenceDiagram
participant User
participant FE as SourcesTab
participant BE as Backend
User->>FE: Klick auf Flag-Button (Knoten X, Flag F)
FE->>FE: pendingToggles.add(X|F) -> Spinner sichtbar
alt Knoten hat keinen DB-Record
FE->>BE: POST /datasources oder /feature-datasources
BE-->>FE: { id }
end
FE->>BE: PATCH /api/datasources/{id}/{F} Body: {F: !effective}
BE-->>FE: 200 OK
FE->>BE: POST /tree/children Body: {parents: [null, ...expandedKeys]}
BE-->>FE: nodesByParent (alle effective* aktualisiert)
FE->>FE: setChildrenByParent + pendingToggles.delete -> Re-Render
```
State (im Component):
- `childrenByParent: Map<string, TreeNode[]>`
- `expandedKeys: Set<string>`
- `pendingToggles: Set<string>` (Key-Form `${nodeKey}|${flag}`)
Re-fetch wird ueber einen Signatur-Effect getriggert, sobald `expandedKeys` aendert.
Generischer `_FlagButton` rendert:
- Spinner waehrend `pending`
- Mixed-Symbol (`U+25E9`, "square with diagonal lines") wenn `effective* === "mixed"` (gilt einheitlich fuer alle 3 Flags)
- Sonst Flag-spezifisches Icon (Lock fuer neutralize, Brain fuer RAG, Personen/Building/Globus fuer Scope)
### FDS-RAG-Inheritance
- `_INHERITABLE_FDS_FLAGS` enthaelt nun `ragIndexEnabled`.
- `FeatureDataSource.ragIndexEnabled: Optional[bool]` Field (Default `None` = inherit; Auto-Migration via `ALTER TABLE ADD COLUMN`).
- `PATCH /api/datasources/{id}/rag-index` akzeptiert sowohl DataSource- als auch FeatureDataSource-IDs (via `_findSourceRecord`). Bootstrap-Job und Chunk-Purge bleiben DataSource-only (FDS-RAG ist Feature-Pipeline-getrieben, Flag genuegt).
- `GET /api/workspace/{instanceId}/feature-datasources` liefert `effectiveRagIndexEnabled`.
- `resolveEffectiveForFds` liefert den dritten Wert konsistent mit dem DS-Pendant.
## Bewusste Reduzierung
FDS-Records (z.B. "Mandant Mueller" als einzelner Datensatz innerhalb der `Mandanten`-Tabelle) sind im neuen Tree nicht expandierbar (`hasChildren: False` fuer `fdsTable`). Begruendung: User-Entscheidung 2026-05-18, Tabellen-Ebene reicht. Das `FeatureDataSource.recordFilter`-Feld bleibt im Datenmodell und in `POST /feature-datasources` fuer API-Kompatibilitaet erhalten, wird aber von der UI nicht mehr gesetzt.
## Entfernter Code (Backend)
Vollstaendig geloescht (keine Aufrufer mehr im Frontend, kein deprecated-Zustand):
- `POST /api/workspace/{instanceId}/datasources/resolve-flags` und Models `_ResolveFlagsRequest`, `_ResolveFlagItem`, `_ResolveFlagFdsItem`
- `GET /api/workspace/{instanceId}/connections`
- `GET /api/workspace/{instanceId}/connections/{connectionId}/services`
- `GET /api/workspace/{instanceId}/connections/{connectionId}/browse`
- `GET /api/workspace/{instanceId}/feature-connections`
- `GET /api/workspace/{instanceId}/feature-connections/{fiId}/tables`
- `GET /api/workspace/{instanceId}/feature-connections/{fiId}/parent-objects/{tableName}`
Erhalten (haben weitere Konsumenten ausserhalb UDB):
- `GET /api/workspace/{instanceId}/datasources` -> useWorkspace.ts, WorkspacePage.tsx, Commcoach*, GraphicalEditorPage.tsx
- `GET /api/workspace/{instanceId}/feature-datasources` -> selbe Konsumenten
- `POST .../datasources` und `POST .../feature-datasources` -> neuer SourcesTab nutzt sie fuer `_ensureRecord`
## Entfernter Code (Frontend)
- Komplettes altes `SourcesTab.tsx` (~2500 Zeilen): `_resolveFdsFlags`, `resolvedFdsFlags`, `_findCoveringDs`, `_readFdsFlags`, alle optimistic `setDataSources(prev.map(...))` und `setFeatureDataSources(prev.map(...))`, prop-drilling `inheritedScope`/`inheritedNeutralize`, mehrere Sub-Komponenten (`_FeatureTableRow`, `_FeatureActionContext`, etc.)
- Totes CSS-Modul `SourcesTab.module.css`
## Tests
Neu:
- `gateway/tests/unit/services/test_buildTree.py` (10 Tests): Key-Encoding/Decoding, `_effectiveTripletDs/Fds` Defaults+Inheritance, Record-Lookup (DS path-normalisation; FDS record-filter equality), `getChildrenForParents`-Smoke (unknown parent -> `[]`, leeres Top-Level).
- 5 neue Tests in `test_inheritFlags.py::TestResolveEffectiveForFds`: RAG inherits when only neutralize overridden; RAG aggregate-mixed; `_INHERITABLE_FDS_FLAGS` contains all 3 keys.
- `TestCascadeResetFdsRag::test_cascade_resets_rag_on_descendants`.
Resultat: `tests/unit/services/test_inheritFlags.py` + `test_buildTree.py` = 82/82 gruen.
Frontend: `npx tsc --noEmit --skipLibCheck` clean.
## Verifizierte Datei-Pfade
- Backend: `gateway/modules/serviceCenter/services/serviceKnowledge/_buildTree.py`, `_inheritFlags.py`, `gateway/modules/features/workspace/routeFeatureWorkspace.py`, `gateway/modules/routes/routeDataSources.py`, `gateway/modules/datamodels/datamodelFeatureDataSource.py`
- Frontend: `frontend_nyla/src/components/UnifiedDataBar/SourcesTab.tsx`
- Tests: `gateway/tests/unit/services/test_buildTree.py`, `test_inheritFlags.py`

View file

@ -12,8 +12,21 @@ type: `feat` `fix` `refactor` `docs` `test` `chore` `build` · scope: `gateway
Skip: reine Refactors, Formatting, Lint, Dep-Bumps, Test-only, Wiki-Tippfehler.
## 2026-05-19
- 2026-05-19 | fix | frontend-nyla+gateway | **RAG-Inventar Mandate-Scope 403 + SQL-Fehler** — Zwei Bugs: (1) Frontend sendete `mandateId` als Query-Parameter statt `X-Mandate-Id`-Header → `context.mandateId=None` → 403. Fix: Header wird gesetzt. (2) Backend `routeRagInventory._getInventoryMandate` filterte `UserConnection` mit `recordFilter={"mandateId": ...}`, aber `UserConnection` hat kein `mandateId`-Feld → SQL-Error. Fix: Mandate-Members via `UserMandate`-Junction holen, dann deren `getUserConnections(userId)` aggregieren.
- 2026-05-19 | refactor | gateway+frontend-nyla | **UDB Toggle Refresh: Visible-IDs Pattern** -- After a toggle, FE sends all visible node IDs in one POST request; backend returns correct attribute values (incl. mixed) for exactly those IDs. No full-tree reload, no optimistic updates. New endpoints: `POST /api/files/attributes` (FilesTab) and `POST /api/workspace/{id}/tree/attributes` (SourcesTab). `TreeNodeProvider.refreshAttributes(ids)` added to interface. Collapse now removes children from FE state (always fresh on re-expand). Removed `_enrichFoldersWithMixed` and `_refetchAllExpanded`.
## 2026-05-18
- 2026-05-18 | fix+feat | gateway+frontend-nyla | **UDB Sources Recovery -- Zweite Smoke-Test-Runde (H1-H10)** (`c-work/3-validate/2026-05-udb-sources-recovery.md`). (H1) Logik-Audit aller drei Flags zusammengefasst: `'mixed'` ist Backend-Aggregate-Anzeige, niemals persistiert; FE-Handler mappen `'mixed'` immer auf konkreten Wert (`'personal'`/`false`); Cascade-PATCH setzt alle Nachkommen auf `NULL` und dann den Master, deshalb kann aggregate nach einem User-Toggle nicht `'mixed'` bleiben. (H2/H8) Initial-Render-Bug: sequentieller `for ... await` im Auto-Expand-Effekt loeste Cancellation-Race aus -- erstes `setNodes` triggerte cleanup, weitere defaultExpanded-Knoten blieben "expanded ohne Children". Fix: `Promise.all(...)` parallel + atomares Single-`setNodes`. Zusaetzlich `autoExpandedRef.current.clear()` in `_loadRoot` (StrictMode-Doppelmount + manuelle Refresh tauglich). (H3) `PUT /api/files/{id}` 404: Route hatte keinen `RequestContext`, `interfaceDbManagement.getInterface(currentUser)` ohne mandate/featureInstance scope -> RBAC-Filter excludiert File. Fix: Context-Dep + scope-pass-through bei `update_file` und `delete_file`; `move_folder` akzeptiert sowohl `parentId` als auch `targetParentId` aus dem Body. (H4) Neuer Folder erschien initial auf Legacy-Top-Level: `FolderFileProvider.createChild(parentId=null, ...)` setzt jetzt `parentId = _SYNTH_ROOT_ID('own')` damit der neue Folder unter `/` rendert. (H5) `+`-Button pro Folder im FormGeneratorTree: neue `onCreateChild?: (parentId: string) => void`-Prop in TreeNodeRow + verdrahtetem `_createFolderAt(parentId)` Handler in der Hauptkomponente; sichtbar im Hover-Action-Slot fuer alle Folders mit `provider.createChild` && `provider.canCreate(id)`. (H6) FilesTab neutralize partly broken: Diagnose plausibel mit H2-Fix erledigt (Optimistic-Update verfehlte Knoten, deren Children durch das Cancellation-Race nicht im FE-State waren). (H7) Hardcoded `[Persoenliche Quellen]`: Source-String hatte `oe`-Encoding, `t()` returned key fuer DE -> sichtbar mit fake-Umlaut. Fix: `resolveTextSafe("Persoenliche Quellen")` -> `resolveTextSafe("Persönliche Quellen")` (echter Umlaut). (H9) FDS-Neutralize ging nicht: `POST /api/workspace/{instanceId}/feature-datasources` 403te Cross-Mandate-Erstellung (Workspace mandate A, Feature mandate B beides user-zugaenglich), `_ensureRecord` schluckte den Fehler still. Fix: Cross-Mandate-Block entfernt, statt dessen `getFeatureAccess(userId, body.featureInstanceId)`-Validierung; `mandateId` der neuen FDS = `wsMandateId` (= Workspace-Tenancy, konsistent mit Tree-Filter). (H10 = G5) Persistenter Expand-State umgesetzt: `WorkspaceUserSettings.uiTreeExpansion: Dict[str, List[str]]` Backend-Field; neue Routen `GET/PUT /api/workspace/{instanceId}/ui-tree-expansion/{scope}`; FE-Hook `useTreeExpansion(instanceId, scope)` mit 600 ms Debounce-PUT; `FormGeneratorTree` Props `expandedIds?: string[] | null` + `onExpandedIdsChange?: (ids) => void` (controlled mode); `SourcesTab` (scope `'sources'`), `FilesTab` (scopes `'filesOwn'` + `'filesShared'`) verdrahtet. Bei `null` (kein Record) Default-Verhalten + erster Toggle erstellt den Settings-Record; bei Array gewinnt persistierte Liste ueber Backend-`defaultExpanded`-Hints. Tests: Backend `services+routes` 107/107 gruen, Frontend UDB+FormGen 69/69 gruen.
- 2026-05-18 | fix+feat | gateway+frontend-nyla | **UDB Sources Recovery -- Smoke-Test-Followups (G1-G5)** (`c-work/3-validate/2026-05-udb-sources-recovery.md`). (G1) `tsconfig.json` referenziert jetzt auch `tsconfig.test.json`; Testfile bekam expliziten `import React`, `afterEach`, `@testing-library/jest-dom/vitest`. 139 -> 0 Lints; Tests 50/50 weiterhin gruen. (G2) Off-State der `neutralize`/`ragIndexEnabled`-Buttons: `filter: grayscale(1); opacity: .45` im OFF-Zustand -- offenes Schloss + greyed-Brain klar als "deaktiviert" lesbar. (G3a) Bug `_browseChildren`: Childs erbten `parentKey=ds|...|/` statt des aufgerufenen `svc|...`; Children erschienen daher nie im FE-Tree. Fix: `parentKey` als optionaler Parameter durchgereicht; Dispatcher uebergibt den asked-for-Key. (G3b) Per-Field-Neutralize fuer Feature-Tabellen: `fdsTable.hasChildren=True` bei vorhandener `meta.fields`-Liste; neuer Knoten-Typ `fdsField` (Dispatcher `fdstbl|fi|table` -> `_featureTableFields`). Effektiver Field-Neutralize = `parent.neutralize OR field IN parent.neutralizeFields`. Toggle: `UdbSourcesProvider.patchNeutralize` splittet die Batch nach Kind und ruft fuer fdsField `PATCH /api/datasources/{id}/neutralize-fields` mit der mutierten Liste; `neutralizeFields` ist im Tree-Payload mitgesendet -- kein Extra-GET. Scope/RAG auf Feldebene bewusst nicht editierbar. +3 neue Backend-Tests in `TestFeatureTableFields`. (G4) FilesTab synthetischer "/"-Root pro Ownership: Provider mappt `parentId=null` auf `[__filesRoot:<own|shared>]` (`defaultExpanded=true`); reale Top-Level-Items werden Children. `moveNodes` mit Synth-Root-Ziel re-mapt auf `parentId/folderId=null` (= Drop auf "/"); `patchScope`/`patchNeutralize` mit Synth-Root-Id materialisieren ueber API alle Top-Level-Folders+Files und setzen pro Folder `cascadeChildren=true` -- globaler Toggle ohne Backend-Aenderung. Tests: Backend 19/19 gruen, Frontend UDB+FormGen 69/69 gruen.
- 2026-05-18 | refactor | gateway+frontend-nyla | **UDB Sources Recovery -- Folge-UX-Iteration** (`c-work/3-validate/2026-05-udb-sources-recovery.md`). Auf das User-Feedback nach Phase 1-3: (a) **Visual cascade fuer `neutralize` und `ragIndexEnabled`** wieder klar erkennbar -- vorher *gleiches Symbol* fuer on/off (nur Opacity), Cascade unsichtbar. Distinct emojis: `_NEUTRALIZE_ON_EMOJI=closed lock` / `_NEUTRALIZE_OFF_EMOJI=open lock`, `_RAG_ON_EMOJI=brain` / `_RAG_OFF_EMOJI=thought-bubble`. Backend-Cascade war korrekt (parametrisch in `cascadeResetDescendants(rec, flag)`); rein UI-Issue. (b) **Top-Level-Layout flach**: synthetische Wrapper `srcRoot` + `mandateRoot` entfernt. `_topLevel` emittiert direkt `[personalRoot, mgrp|m1, mgrp|m2, ...]`. Mandate-Groups haben nun `parentKey=None`. (c) **`TreeNode.defaultExpanded?: boolean`** als generisches Tree-Feature (one-shot pro Node-Id ueber `autoExpandedRef`); BE markiert `personalRoot` und alle Mandate-Groups mit `defaultExpanded=True` -- UI oeffnet bis zur Datenquellen-Ebene ohne User-Klick. (d) **Icon-Reihenfolge** in `FormGeneratorTree` (rechtsbuendig, von rechts nach links): neutralize, scope, sendToChat, rag, settings. Settings (`extraActions`) jetzt linksbuendig. (e) **Settings-Icon nur auf Daten-Quellen-Root** (`kind in {connection, featureNode}`), aber dort *immer* sichtbar, auch ohne `dataSourceId`; Klick triggert lazy `_ensureRecord` -> `onOpenSettings`. (f) **`SourcesTab`** setzt `title={t('Datenquellen')}` als Section-Header. Tests: Backend `test_buildTree.py` 16/16 + `test_inheritFlags.py` 72/72 -> 88/88 gruen. Frontend `FormGeneratorTree.test.tsx` 50/50 (+`defaultExpanded`-Tests) + `UdbSourcesProvider.test.ts` 16/16 (+settings-on-root + defaultExpanded-passthrough). TypeScript clean.
- 2026-05-18 | refactor | gateway+frontend-nyla | **UDB Sources Recovery: SourcesTab nutzt FormGeneratorTree** (`c-work/3-validate/2026-05-udb-sources-recovery.md`). Behebt das Folge-Problem aus dem Generic-Tree-Refactor vom selben Tag, dass der Sources-Tab nur "Lade Datenquellen..." zeigte, weil der ad-hoc `_TreeNodeView`-Renderer in `SourcesTab.tsx` an einem State-Race (StrictMode-Doppelmount × `mountedRef`-Frueh-Abort × paralleler `expansionSignature`-useEffect, vier sichtbare POSTs) erstickte. Diagnose via temporaerem `_buildTree.getChildrenForParents`-INFO-Log: Backend liefert sauber `counts={'__root__': 6}` -- Bug war ausschliesslich auf der ad-hoc-Renderer-Seite. **Backend** (`_buildTree.py`): synthetische Container `srcRoot` (Top-Level), `personalRoot`, `mandateRoot` mit neuem `_syntheticNode` Helper und `displayOrder`-Sort-Hint; `_topLevel` schrumpft auf `[srcRoot]`, `_srcRootChildren` emittiert `[personalRoot, mandateRoot]`, `_personalRootChildren` und `_mandateRootChildren` enthalten die alte Connections-/Mandate-Logik mit korrigierten `parentKey`. Dispatcher in `getChildrenForParents` matcht `parentKey == _KEY_*_ROOT` vor dem Decode-Pfad. +7 neue `TestSyntheticRoots` Tests, Service-Suite 89/89 gruen. **FormGeneratorTree** (generisch erweitert, kein UDB-Vokabular): `TreeNode.ragIndexEnabled?: boolean | 'mixed'`, `TreeNode.displayOrder?: number`, `TreeNodeProvider.patchRagIndex?` + `canPatchRagIndex?`, `FormGeneratorTreeProps.refreshAfterAction?: boolean`. Sort-Comparator setzt `displayOrder` vor folder-first. Dritter Flag-Button (`_RAG_EMOJI`, `_ACTION_RAG`) rendert nur wenn `ragIndexEnabled !== undefined`. `_runAction` ruft `_refetchAllExpanded` automatisch auf wenn `refreshAfterAction=true`; optimistic-Update-Pfad fuer FilesTab unveraendert. +10 neue Tests, Tree-Suite 51/51 gruen. **Frontend SourcesTab**: ad-hoc-Renderer mit `_TreeNodeView`, `_FlagButton`, eigenem `pendingToggles`-State, eigenem `_fetchVisible`-Pfad komplett geloescht. Neuer `UdbSourcesProvider.tsx` mappt Backend-`UdbBackendNode` auf generischen `TreeNode<T>` (Synthese-Container ohne Flag-Felder); haelt internen `nodeCache: Map<key, UdbBackendNode>` fuer Patch-Pfade; `_ensureRecord` POSTet `/datasources` oder `/feature-datasources` je nach `kind`; danach PATCH `/api/datasources/{id}/{scope|neutralize|rag-index}`. Neue `SourcesTab.tsx` ist 75 LOC duenner Wrapper: `useMemo` Provider + `<FormGeneratorTree compact selectable={false} allowCreateFolder={false} refreshAfterAction />` + Settings-Modal-Anbindung ueber `extraActions`. +14 neue Provider-Tests, Tree+Provider-Suite 65/65 gruen.
- 2026-05-18 | fix | gateway | PostgreSQL `recordCreate`/`_save_record`: Zeichenketten-Parameter mit eingebetteten NUL-Bytes (`\\x00`) werden vor dem Bind gestriped — behebt `ingestion.failed` / `ContentChunk`-Insert (psycopg2: „A string literal cannot contain NUL“), z.B. bei indexierten `.sql`/Dump-Dateien.
- 2026-05-18 | refactor | gateway+frontend-nyla | **UDB Generic Tree Refactor: Backend autoritativ, FE pure Renderer, RAG fuer FDS** (`c-work/4-done/2026-05-udb-generic-tree-refactor.md`). Schliesst die ueber mehrere Iterationen verschleppte UI-Inkonsistenz, dass Toggles unter bestimmten Bedingungen keinen UI-Effekt hatten. Root-Cause: parallele Logik im Frontend (`_resolveFdsFlags`, optimistic Updates, `_findCoveringDs`) widersprach den vom Backend gelieferten Werten. **Architektur**: Ein einziger Endpoint `POST /api/workspace/{instanceId}/tree/children` mit Body `{parents: [null, ...expandedKeys]}` liefert `nodesByParent` als flachen Map mit allen drei `effective*`-Werten (`neutralize`, `scope`, `ragIndexEnabled` als `boolean|'mixed'` bzw. `string|'mixed'`) pre-computed auf der `mode='aggregate'`-Ebene. Neuer Orchestrator `serviceKnowledge/_buildTree.py` mit stabilem Key-Format (`conn|<id>`, `svc|<connId>|<service>`, `ds|<connId>|<sourceType>|<path>`, `mgrp|<mid>`, `feat|<mid>|<code>|<fiId>`, `fdstbl|<fiId>|<table>`) — preloadet DS+FDS einmal pro Request und dispatched pro Parent-Kind an Connector/Catalog/RBAC. Sourcetype-Mapping (`sharepoint -> sharepointFolder` etc.) ist nun Backend-autoritativ. **FE-Refactor**: `SourcesTab.tsx` von ~2500 auf ~530 Zeilen reduziert. State nur noch `childrenByParent: Map`, `expandedKeys: Set`, `pendingToggles: Set`. Toggle-Flow strikt PATCH -> Refetch -> Render mit Spinner ueber dem Flag-Button bis API-Response zurueck ist. Generischer `_TreeNodeView` rekursiv fuer ALLE Kinds, generischer `_FlagButton` mit einheitlichem Mixed-Symbol (`U+25E9`, "square with diagonal lines") fuer alle drei Flags. **RAG fuer FDS**: `_INHERITABLE_FDS_FLAGS` enthaelt nun `ragIndexEnabled`; `FeatureDataSource.ragIndexEnabled: Optional[bool]` Field (Auto-Migration via `ALTER TABLE ADD COLUMN`); `PATCH /api/datasources/{id}/rag-index` akzeptiert via `_findSourceRecord` sowohl DS als auch FDS (Bootstrap-Job + Chunk-Purge bleiben DS-only); `GET /feature-datasources` liefert `effectiveRagIndexEnabled`; `resolveEffectiveForFds` um den dritten Wert ergaenzt. **Geloescht** (keine Konsumenten mehr): `POST /datasources/resolve-flags` + 3 Models, `GET /connections`, `GET /connections/{id}/services`, `GET /connections/{id}/browse`, `GET /feature-connections`, `GET /feature-connections/{fiId}/tables`, `GET /feature-connections/{fiId}/parent-objects/{tableName}`, totes `SourcesTab.module.css`. **Bewusste Reduzierung** (User-Entscheidung): FDS-Records sind nicht mehr expandierbar (`hasChildren: false` fuer `fdsTable`); Tabellen-Ebene reicht. `FeatureDataSource.recordFilter` bleibt fuer API-Kompat erhalten. **Tests**: neu `test_buildTree.py` (10 gruen — Key-Coding, Effective-Triplets, Record-Lookup, Orchestrator-Smoke), +5 neue in `test_inheritFlags.py::TestResolveEffectiveForFds` (RAG inherits, RAG aggregate-mixed, `_INHERITABLE_FDS_FLAGS` enthaelt RAG) +1 in `TestCascadeResetFdsRag`. Resultat: 82/82 gruen; Frontend `npx tsc --noEmit` clean.
- 2026-05-18 | feat | gateway+frontend-nyla | **UDB Toggle-Semantik v2: 'mixed'-Aggregation + bottom-up cascade + Backend-autoritative effective values** — Grundlegendes Redesign der Toggle-Semantik (neutralize, scope, ragIndexEnabled) für DataSource und FeatureDataSource. (1) Backend: `getEffectiveFlag` und `getEffectiveFlagFds` mit `mode='walk'|'aggregate'` — walk gibt immer konkret, aggregate gibt 'mixed' wenn Subtree divergiert. (2) `cascadeResetDescendants`/`Fds` jetzt bottom-up (deepest-first) für Crash-Sicherheit, Rückgabe `List[str]` statt `int`. (3) GET /datasources und /feature-datasources liefern computed `effectiveNeutralize`, `effectiveScope`, `effectiveRagIndexEnabled` pro Item. (4) PATCH-Response enthält `resetDescendantIds` + `updatedAncestors` für optimistic UI. (5) Bug-Fixes B1-B6: routeDataConnections consent-re-enable RAG, routeRagInventory reindex+inventory, routeDataSources settings-RBAC, _dataSourceTools download neutralize, _featureSubAgentTools FDS neutralize cascade — alle auf `getEffectiveFlag(mode='walk')` umgestellt. (6) Frontend: Workarounds entfernt (`_effectiveFlag`, `_findAncestorDs`, `_AUTHORITY_SOURCE_TYPES`, prop-drilling `inheritedScope/Neutralize`), zentrale `_readDsFlags`/`_readFdsFlags` aus Backend-Werten, Toggle-Handler mit optimistic cascade via `resetDescendantIds`. (7) 'mixed'-Icon-Darstellung im UI.
- 2026-05-18 | chore | gateway | RAG-Walker und `requestIngestion`: per-item Lognois reduziert — `subWalkerHelpers` (`walker.item.start`, `walker.download.start`, `walker.extract.start`) und `ingestion.skipped.duplicate` von INFO auf DEBUG, damit Daily-Resync / wiederholtes Durchlaufen nicht das App-Log flutet; Stuck-Triage via DEBUG auf `modules.serviceCenter.services.serviceKnowledge.subWalkerHelpers` bzw. `mainServiceKnowledge`.
- 2026-05-18 | fix | gateway+frontend-nyla | **UDB Cascade-Inherit Bugfix: Connection-Root Toggle hatte keinen UI-Effekt** — User-Report: "no change in UI, whatever icon I press". Log-Analyse zeigte: jeder Toggle macht POST `/datasources` (Backend gibt via Upsert die bereits existierende ID zurück) gefolgt von PATCH (Backend updated korrekt), aber UI bleibt unverändert. **Root-Cause 1**: Die ConnectionRoot-DataSource (`sourceType='msft'`/`'google'`/`'clickup'`/`'infomaniak'`/`'local'`, `path='/'`) wurde im Frontend von `_findDs` ignoriert (`if (node.type === 'connection') return undefined`) — Workaround aus dem Vortag, der eine andere Race-Condition verhindern sollte. Folge: `ds === undefined` für Connection-Nodes → `_addAsDataSource`-POST → Backend Upsert findet bestehenden DS via `connectionId+path`-Match und returnt dieselbe ID → PATCH erfolgreich → UI rendert aber weiter mit `ds=undefined` → opacity bleibt 0.35. **Fix 1**: `_findDs` matcht jetzt deterministisch via `sourceType`: für Connection-Nodes `node.authority` ('msft'/'google'/…), für Service-Nodes `_SERVICE_TO_SOURCE_TYPE[node.service]` ('sharepointFolder'/…). Zwei DS mit `connectionId+path='/'` aber unterschiedlichem `sourceType` (Connection-Root vs Service-Root) sind dadurch eindeutig unterscheidbar. **Root-Cause 2**: Cross-Authority-Vererbung fehlte komplett. User-Erwartung: Toggle auf Connection-Root muss alle Service-Children (SharePoint, OneDrive, Outlook unter derselben msft-Connection) visuell mitziehen. Backend `getEffectiveFlag` und `cascadeResetDescendants` filterten aber strikt auf `sourceType==parentSourceType`, also keine Vererbung über die Authority-Grenze hinweg. **Fix 2**: Neue Konstante `_AUTHORITY_SOURCE_TYPES = {'local','google','msft','clickup','infomaniak'}` in `_inheritFlags.py` und Mirror im Frontend. `_findAncestorChain` ergänzt um den ConnectionRoot als "äusseren" Ancestor (after all same-sourceType ancestors). `cascadeResetDescendants` cascadiert bei `parentIsConnectionRoot=True` über die GANZE Connection (cross-sourceType). Frontend `_findAncestorDs` analog. **Tests**: 23/23 grün (4 neue für Cross-Authority: `test_connection_root_inherits_cross_sourcetype`, `test_same_sourcetype_ancestor_wins_over_connection_root`, `test_connection_root_does_not_self_inherit`, `test_connection_root_cascades_cross_sourcetype`). Frontend `npm run build` + `tsc --noEmit` grün.
- 2026-05-18 | feat | gateway+frontend-nyla | **UDB Cascade-Inherit für DataSource-Flags (`neutralize`, `ragIndexEnabled`, `scope`)** — Plan: `c-work/1-plan/2026-05-udb-cascade-inherit.md`. Schliesst die Konsistenz-Lücke, dass Toggles an einem Parent nicht nach unten propagieren konnten, sobald ein Descendant einen eigenen DataSource-Record hatte. Beispiel-Bug: SharePoint=true → Folder1=false (explizit) → SharePoint=true (zweites Toggle) → Folder1 blieb auf false statt mit dem Parent zu folgen. **Architektur**: 3-wertige Felder mit `NULL = inherit`, expliziter `True/False` (oder `'personal'/'mandate'/'platform'/'global'` für scope). **Backend**: (1) Datenmodell `DataSource.{neutralize,ragIndexEnabled,scope}` und `FeatureDataSource.{neutralize,scope}` auf `Optional[...]` umgestellt; Migration `script_db_migrate_datasource_inherit.py` macht Spalten nullable (idempotent, additiv — bestehende Werte bleiben explizit, NEW records starten mit NULL). (2) Neuer zentraler Helper `serviceKnowledge/_inheritFlags.py` mit `getEffectiveFlag(rec, flag, allDs)` (path-traversal aufwärts, longest-prefix wins, defensiv `connectionId` UND `sourceType` als Filter), `cascadeResetDescendants(rootIf, parentRec, flag)` (setzt nur das EINE Flag der explizit-belegten Descendants auf NULL, andere Flags unangetastet) und `_isAncestorPath` (`/` ist Ancestor von allem; `/foo` NICHT von `/foobar``/`-Separator-Pflicht). (3) PATCH-Endpoints `routeDataSources.{_updateDataSourceScope,_updateDataSourceNeutralize,_updateDataSourceRagIndex}` akzeptieren `Optional[bool|str]` als Body-Wert: `null` → reset auf inherit (kein Cascade); `true/false` → explizit + Cascade-Reset aller Descendants. RAG-Endpoint zusätzlich: nur bei `True` Mini-Bootstrap-Job + `_ensureConnectionKnowledgeFlag`; nur bei `False` synchrone Chunk-Purge — `null` no-op. Audit-Log `rag_index_toggled` trägt `cascadedDescendants` Count. (4) Walker-Integration: `_loadRagEnabledDataSources` filtert auf **effective** ragIndexEnabled (via `getEffectiveFlag`) und schreibt resolved Werte für `neutralize/scope` direkt in das returned dict — Walker (Sharepoint/Outlook/Gdrive/Gmail/Clickup/Kdrive) bleiben unverändert und lesen weiter `ds.get("neutralize", False)`. Alter `subPolicyResolver.py` zu Backward-Compat-Shim auf `getEffectiveFlag` deprecated. **Frontend**: `UdbDataSource` Interface mit `boolean|null` und `string|null`; neue Helper `_findAncestorDs` + `_effectiveFlag` (path-traversal analog zu Backend); Toggle-Handler senden `!currentEffective` (= aktuell sichtbarer Wert wird invertiert), Backend macht den Cascade. Local State: optimistisch + `_fetchDataSources()` nach PATCH damit cascade-resettete Descendants aus DB neu geladen werden. Alte unbenutzte `_togglePersonalNeutralize/_togglePersonalRagIndex/_cyclePersonalScope` entfernt. **Tests**: neuer `test_inheritFlags.py` mit 19 grün — explicit-wins, ancestor-traversal, default-fallback, sourceType-Isolation, connectionId-Isolation, `/foo` ≠ Ancestor von `/foobar`, root-is-ancestor-of-everything, scope-string-default, cascade-touches-only-target-flag, cascade-skips-other-sourcetypes. Bestehende `test_knowledge_ingest_consumer.py` 8/8 grün. Frontend `npm run build` grün, TypeScript clean. **Pre-existing Test-Failures** (nicht durch diesen PR): `test_p1d_consent_prefs.py` (5) — Python-3.13 asyncio-Deprecation + Schema-Drift `ConnectionIngestionPrefs.neutralizeBeforeEmbed`; `test_bootstrap_sharepoint.py` + `test_bootstrap_gmail.py` (5) — rufen `bootstrapSharepoint/Gmail` ohne `dataSources=` auf, was seit dem 2026-04 Unified-Indexing-PR den Early-Return triggert.
@ -325,4 +338,10 @@ Skip: reine Refactors, Formatting, Lint, Dep-Bumps, Test-only, Wiki-Tippfehler.
- 2026-05-12 | fix | service-teams-browser-bot | Auth bot PiP loop fix: in full Teams web app, chat toggle navigates to Chat section → PiP → periodic scan reopens → PiP again (endless loop). Fix: added isAuthMode flag to ChatProcedure; in auth mode: skip chat panel toggle entirely, skip periodic reopen, skip panel-open preflight for sendChatMessage. Chat send uses direct input lookup in the meeting view. Replaced _returnToMeetingIfPip() approach (reactive) with prevention (don't toggle)
- 2026-05-12 | fix | service-teams-browser-bot | Auth bot meeting minimized (PiP): chat button click hit Teams sidebar "Chats" navigation instead of in-meeting toggle → meeting minimized, all PeerConnections closed, video/audio broke. Fix: isDangerousNavButton filter excludes sidebar nav, tab-items, submenu triggers; aria-pressed fallback for panel detection in full Teams web app; _openMoreMenu scoped to calling toolbar
- 2026-05-11 | fix | service-teams-browser-bot | Anonymous bot stuck after lobby admission: _waitForMeetingAdmission now tracks lobby-to-meeting transition, waits patiently when lobby vanishes (= admitted), and reloads page as recovery; isInMeeting expanded with light-meetings + German selectors
- 2026-05-18 | feat | gateway+frontend | UDB Resolve Endpoint: neuer POST /datasources/resolve-flags Bulk-Endpoint liefert effective-Werte (neutralize, scope, ragIndexEnabled) fuer beliebige Pfade auch ohne eigenen DB-Record; Frontend entfernt client-seitige Vererbungslogik (_findCoveringDs), nutzt ausschliesslich Backend-Daten via resolvedFlags Map
- 2026-05-19 | fix | gateway | RAG Inventory /mandate: ImportError UserMandate behoben (war in datamodelUam statt datamodelMembership); refactored zu getUserMandatesByMandate(); RBAC membership-Check hinzugefuegt (verhindert Zugriff auf fremde Mandate via X-Mandate-Id)
- 2026-05-19 | fix | gateway+frontend | RAG Inventory Mandate-Dropdown: neuer Endpunkt GET /api/rag/inventory/my-mandates liefert nur Mandate mit aktiver Mitgliedschaft; Frontend nutzt diesen statt /api/mandates/ (admin-only Route)
- 2026-05-19 | feat | gateway+frontend | RAG Inventory Feature-Daten: featureInstances-Array mit Datei-/Chunk-Zaehler, Status-Breakdown (indexed/pending/failed), FeatureDataSource-Details mit ragIndexEnabled-Flag, RAG-Aktiv-Indikator; Frontend zeigt Sync-Status-Banner, Datenquellen pro Instanz, RAG-Hinweis wenn inaktiv; leere Instanzen werden ausgefiltert
- 2026-05-19 | feat | gateway+frontend | Feature-Daten RAG-Sync: neuer Background-Job-Handler feature.bootstrap (subFeatureBootstrap.py) indexiert FeatureDataSource-Tabellendaten via FeatureDataProvider+requestIngestion; Handler-Registrierung in registerKnowledgeIngestionConsumer; neuer Endpoint POST /api/rag/inventory/reindex-feature/{workspaceInstanceId} mit FeatureAccess-Check; _buildFeatureInstanceInventory liefert runningJobs/lastSuccess/lastError; Frontend: Job-basierte Sync-Status-Banner + Reindex-Button fuer Feature-Instanzen (analog zu Connection-Sync)
- 2026-05-19 | fix | gateway | FeatureDataProvider: alle DB-Zugriffe (getActualColumns, browseTable, aggregateTable, queryTable, _resolveInstanceColumn) von deprecated db.connection.cursor() auf db.borrowCursor() migriert (Pool-API-Kompatibilitaet nach Pooling-Refactoring)