From 57579141cf760df967daf8b80408671730315ce9 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Mon, 18 May 2026 00:26:13 +0200
Subject: [PATCH] data source flag cascading issue
---
TOPICS.md | 1 +
b-reference/gateway/ai-agent.md | 30 ++-
b-reference/gateway/architecture.md | 43 +++-
c-work/1-plan/2026-05-udb-cascade-inherit.md | 150 ++++++++++++++
.../2026-05-udb-datasource-settings.md | 187 ++++++++++++++++++
...-feature-data-agent-ontology-and-repair.md | 0
c-work/_CHANGELOG.md | 4 +
7 files changed, 411 insertions(+), 4 deletions(-)
create mode 100644 c-work/1-plan/2026-05-udb-cascade-inherit.md
create mode 100644 c-work/2-build/2026-05-udb-datasource-settings.md
rename c-work/{3-validate => 4-done}/2026-05-feature-data-agent-ontology-and-repair.md (100%)
diff --git a/TOPICS.md b/TOPICS.md
index da9112d..2f50318 100644
--- a/TOPICS.md
+++ b/TOPICS.md
@@ -49,6 +49,7 @@ Lade immer zuerst diese Datei. Dann gezielt die passende(n) Referenz-Datei(en).
| Automation Unification | c-work/1-plan/2026-04-automation-unification.md | Refactoring v1/v2/Workspace |
| Unified Knowledge Indexing (RAG) | c-work/4-done/2026-04-id-unified-knowledge-indexing-rag-concept.md | Ingestion-Fassade `requestIngestion`, Idempotenz, Connector-Lifecycle |
| 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 |
| 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 |
diff --git a/b-reference/gateway/ai-agent.md b/b-reference/gateway/ai-agent.md
index 68cda9f..f9fee38 100644
--- a/b-reference/gateway/ai-agent.md
+++ b/b-reference/gateway/ai-agent.md
@@ -1,6 +1,6 @@
-
-
+
+
# AI Agent & Knowledge Store
@@ -384,6 +384,32 @@ Vor jedem Item ruft der Walker `logItemStart(service, path, sizeBytes, mime)`
- Eltern-DataSource mit explizitem `ragIndexEnabled` vererbt an Kind-Pfade
- Gleiches Pattern wie `neutralize` und `scope`
+#### Konfigurierbare RAG-Limits (ab 2026-05-17)
+
+Walker-Limits (`maxBytes`, `maxFileSize`, `maxItems`, `maxDepth` für File-Walker; `maxTasks`, `maxWorkspaces`, `maxListsPerWorkspace` für ClickUp) sind nicht mehr in den Walker-Modulen hartkodiert, sondern aus zwei Quellen zusammengesetzt:
+
+1. **Zentraler Default** — `modules/serviceCenter/services/serviceKnowledge/_ragLimits.py` (`FILES_LIMITS_DEFAULT`, `CLICKUP_LIMITS_DEFAULT`). Die alten `MAX_*_DEFAULT`-Konstanten in den Walkern sind dünne Aliase und bleiben für Rückwärtskompatibilität bestehen.
+2. **DataSource-Override** — `DataSource.settings.ragLimits.` (oder `FeatureDataSource.settings`). JSONB-Spalte, optional, vollständig vom User editierbar.
+
+**Semantik** (kritisch, weil leicht zu missverstehen):
+- `_ragLimits.getStoredOverrides(ds, kind)` liefert NUR die explizit gesetzten Overrides → Walker mergen sie auf den **caller-supplied** `limits=`-Parameter (Test-Override gewinnt weiterhin).
+- `_ragLimits.getRagLimits(ds, kind)` mergt Overrides auf die globalen Defaults → API/Cost-Estimate-Pfad.
+- **Keine Override-Schicht, kein Resolver, keine Vererbung** für `ragLimits`. Was im Settings-Modal steht, ist exakt das, was der Walker liest.
+
+**Settings-API:**
+
+| Methode | Pfad | Zweck |
+|---------|------|-------|
+| `PATCH` | `/api/datasources/{id}/settings` | Partial-Update auf `DataSource.settings`/`FeatureDataSource.settings`. Nur Top-Level-Key `ragLimits` akzeptiert; unknown keys → 400. Audit-Log: `AuditCategory.PERMISSION/datasource_settings_changed`. Owner-only (Personal); für Mandate-Scope auch Mandate-Admin. |
+| `GET` | `/api/datasources/{id}/cost-estimate` | Indikative USD-Schätzung für einen Voll-Sync mit den aktuellen Limits. Antwort: `{estimatedTokens, estimatedUsd, basis: {kind, limits, assumptions, notes}, sourceId}`. Default-Heuristik: `text-embedding-3-small` @ `$0.02 / 1M Token`, `BYTES_PER_TOKEN=4`, `EXTRACTABLE_FRACTION=0.4`. Quelle: `_costEstimate.py`. |
+
+**UDB Settings-Modal** (`DataSourceSettingsModal.tsx`): einziges UI für DataSource-Settings, geöffnet via ⚙️-Icon pro Tree-Node im `SourcesTab`. Drei Sektionen:
+1. **Connection** — `knowledgeIngestionEnabled` Master-Toggle (= `patchKnowledgeConsent`-Pfad).
+2. **DataSource RAG-Limits** — Editierbare Felder; Bytes-Limits in MB im UI, in Bytes am Backend.
+3. **Kostenschätzung** — Indikativ, nicht-verbindlich, ändert sich live nach `PATCH /settings`.
+
+Das gleiche Modal wird auf der `RagInventoryPage` aus dem Partial-Banner (`stoppedAtLimit`) via "Limit anpassen"-Button geöffnet → User hat direkten Pfad vom Symptom zur Behebung.
+
---
## Teamsbot-Integration (Hybrid-Routing, kein eigenes Toolset)
diff --git a/b-reference/gateway/architecture.md b/b-reference/gateway/architecture.md
index 6132c85..04b39a0 100644
--- a/b-reference/gateway/architecture.md
+++ b/b-reference/gateway/architecture.md
@@ -1,6 +1,6 @@
-
-
+
+
# Gateway -- Architektur
@@ -163,6 +163,45 @@ Felder vom Typ `TextMultilingual` speichern Benutzertexte mehrsprachig. `xx` ist
**Entries-Identitaet:** Ein Entry wird durch `(key, context)` eindeutig identifiziert — derselbe Text kann mit verschiedenen Contexts existieren.
+### BackgroundJob-Progress-Messages
+
+Hintergrundjobs laufen ausserhalb des Request-Kontexts und haben deshalb keinen `_CURRENT_LANGUAGE`-Wert. Walker schreiben deshalb einen **strukturierten i18n-Payload** in die DB und der Route-Handler uebersetzt server-side beim Read -- der Frontend ruft `t()` nie auf Backend-supplied Werten auf (Regel #2).
+
+**1. Walker schreibt strukturiert** (`gateway/modules/serviceCenter/.../subConnectorSync*.py`):
+
+```python
+progressCb(
+ 0,
+ messageKey="{n} Dateien verarbeitet, {indexed} indexiert",
+ messageParams={"n": processed, "indexed": result.indexed},
+)
+```
+
+`messageKey` ist ein **String-Literal** (Pflicht, damit es scanbar bleibt). `JobProgressCallback` speichert `{key, params}` in `BackgroundJob.progressMessageData` (JSONB) und einen deutschen Best-Effort-Fallback in `progressMessage` (fuer Logs/Audit/Legacy).
+
+**2. Key-Registrierung** -- `progressCb(..., messageKey="…")` durchlaeuft NICHT `t()`, deshalb braucht jeder Key ein **string-literales** `t("…")` in der Feature-Registrierungsdatei (siehe `serviceKnowledge/_progressMessages.py`, `features/trustee/mainTrustee.py`). KEINE Schleifen ueber Listen mit `t(variable)` -- jede Zeile muss `t("LITERAL")` sein, sonst nimmt der Boot-Scan den Key nicht in `UiLanguageSet` auf.
+
+**3. Route uebersetzt server-side** (`routeJobs._serialiseJob`, `routeRagInventory`):
+
+```python
+from modules.shared.i18nRegistry import resolveJobMessage
+
+out["progressMessage"] = (
+ resolveJobMessage(j.get("progressMessageData"))
+ or j.get("progressMessage", "")
+)
+```
+
+`resolveJobMessage(messageData)` ruft intern `t(key)` mit der Request-Sprache und substituiert die Params. Das ist analog zu `resolveText(value)` -- es ist der **einzige** zulaessige `t(variable)`-Pfad, weil er innerhalb der i18n-Infrastruktur liegt.
+
+**4. Frontend rendert 1:1** -- `progressMessage` ist bereits uebersetzt, die Render-Stellen lesen das Feld direkt:
+
+```tsx
+{conn.runningJobs[0].progressMessage || t('Synchronisierung läuft...')}
+```
+
+Das `t('Synchronisierung läuft...')` ist ein lokales UI-Fallback (string-literal, ok), das `progressMessage` aus dem Backend geht nicht durch `t()`.
+
## Feature: Trustee -- Daten-Tabellen-Endpunkte
Alle 13 Trustee-Tabellen sind ueber paginierte, RBAC-gefilterte GET-Endpunkte abrufbar. Die sechs CRUD-Modelle (`TrusteeOrganisation`, `TrusteeRole`, `TrusteeAccess`, `TrusteeContract`, `TrusteeDocument`, `TrusteePosition`) haben weiterhin die etablierten REST-Routen; sieben weitere (zuvor nur als JSON-Export oder Aggregat-Endpunkt verfuegbar) wurden ergaenzt:
diff --git a/c-work/1-plan/2026-05-udb-cascade-inherit.md b/c-work/1-plan/2026-05-udb-cascade-inherit.md
new file mode 100644
index 0000000..064eadf
--- /dev/null
+++ b/c-work/1-plan/2026-05-udb-cascade-inherit.md
@@ -0,0 +1,150 @@
+
+
+
+
+# UDB Cascade-Inherit für DataSource-Flags (neutralize, ragIndexEnabled, scope)
+
+## Beschreibung und Kontext
+
+Das aktuelle UDB-Tree hat ein Konsistenz-Loch: Die drei Flags `neutralize`, `ragIndexEnabled`, `scope` auf `DataSource` sind als nicht-nullable Felder modelliert (`bool`/`str` mit Default `false`/`'personal'`). Das Frontend interpretiert "kein DataSource-Record" als "vererbt", aber sobald ein Knoten einen eigenen Record hat, bleibt sein Wert **explizit gesetzt** — auch dann, wenn der Parent später getoggelt wird.
+
+Beispiel-Szenario, das aktuell falsch läuft:
+1. User toggled SharePoint (Level 2) → `neutralize=true`
+2. User toggled Folder1 (Level 3 unter SharePoint) → `neutralize=false` (explizit)
+3. User klickt SharePoint nochmal → `neutralize=false`
+4. User klickt SharePoint nochmal → `neutralize=true`. Erwartung: alle Folders auch true. **Aber**: Folder1 bleibt explizit `false` und überschreibt die Vererbung.
+
+Die User-Direktive: Bei Status-Änderung muss eine **Cascade** durch alle verschachtelten Subobjekte mit explizitem Wert für dieses Flag laufen — diese werden auf "vererbt" zurückgesetzt. Subobjekte, die bereits "vererbt" sind, werden ignoriert (sie folgen automatisch).
+
+## Ziele
+
+1. Klare 3-Wertige Semantik: `null` (vererbt) | `true` | `false` für `neutralize` und `ragIndexEnabled`
+2. Scope analog: `null` (vererbt) | `'personal'` | `'mandate'` | `'platform'`
+3. Cascade beim Toggle resettet alle Descendants mit explizitem Wert für das **gleiche** Flag
+4. Andere Flags des Descendants bleiben unangetastet (z.B. Toggle `neutralize` lässt `ragIndexEnabled` der Descendants in Ruhe)
+5. Walker-Logik (RAG-Engine, Neutralisierungs-Pipeline) berechnet Effective-Value via Path-Traversal
+6. Kein manueller "Reset to inherit" im UI nötig — Vererbung wird ausschliesslich durch Parent-Toggle wiederhergestellt
+
+## Architektur-Entscheidungen
+
+### Datenmodell
+
+`DataSource` (auch `FeatureDataSource` analog):
+- `neutralize: Optional[bool]` (default `NULL` = inherit)
+- `ragIndexEnabled: Optional[bool]` (default `NULL` = inherit)
+- `scope: Optional[str]` (default `NULL` = inherit; legacy default `'personal'` ändern)
+
+Migration: bestehende Records behalten ihre expliziten Werte (`true`/`false`/`'personal'`). Erst neue Records starten mit `NULL`. Damit ist die Migration nicht-destruktiv.
+
+### Effective-Value Computation (Walker + Frontend)
+
+```
+effective(node, flag) = node.flag if node.flag is not None
+ else effective(parent, flag)
+ else False # default für Root mit NULL
+```
+
+Path-Traversal: ein DS-Record gehört zu `(connectionId, path)`. Parent-Pfade werden via String-Operationen ermittelt (`/foo/bar` → Parent `/foo` → Root `/`). Der Root einer Connection ist `path='/'` mit `sourceType=`.
+
+### Cascade beim Toggle
+
+PATCH-Endpoint:
+- `PATCH /api/datasources/{id}/{flag}` mit Body `{value: bool | null}`
+- Bei `null` → reset auf inherit (kein Cascade nötig, da der Knoten selbst dann erbt)
+- Bei `true`/`false` → Cascade:
+ 1. Setze `target.{flag} = value` für den geklickten Knoten
+ 2. Finde alle Descendant-DataSources: `connectionId == target.connectionId AND path STARTSWITH target.path AND path != target.path`
+ 3. Für jeden Descendant mit `descendant.{flag} IS NOT NULL` → setze `descendant.{flag} = NULL`
+ 4. Audit-Log: `datasource_cascade_reset` mit Anzahl betroffener Records
+
+### Walker-Integration
+
+RAG-Walker (`subConnectorSync*.py`) und Neutralisierungs-Pipeline rufen aktuell direkt `ds.neutralize` / `ds.ragIndexEnabled` ab. Das wird zu einem Helper `getEffectiveFlag(ds, flag, allDataSources)`:
+
+```python
+def getEffectiveFlag(ds, flag, allDsByConnection):
+ """Path-traversal von ds aufwärts bis zum ersten DS mit explizitem Wert."""
+ current = ds
+ while current is not None:
+ value = getattr(current, flag)
+ if value is not None:
+ return value
+ current = _findParentDs(current, allDsByConnection)
+ return False # default
+```
+
+Wichtig: Der Walker iteriert über Files/Folders, nicht über DataSources. Aber die Entscheidung "neutralisieren ja/nein" wird pro Item getroffen. Der Item-Path wird auf den nächsten DS mit explizitem Flag-Wert gemappt. Falls kein expliziter Wert in der Kette, wird der Root-DS verwendet (`path='/'`).
+
+### Frontend
+
+`UdbDataSource`-Interface:
+- `neutralize: boolean | null`
+- `ragIndexEnabled: boolean | null`
+- `scope: string | null`
+
+Effective-Value-Computation analog zu Backend (path-basiert):
+```typescript
+function _effectiveValue(ds, allDs, flag) {
+ if (ds[flag] !== null && ds[flag] !== undefined) return ds[flag];
+ const parent = _findParentDs(ds, allDs);
+ if (parent) return _effectiveValue(parent, allDs, flag);
+ return false;
+}
+```
+
+UI-Verhalten beim Toggle:
+- 2-state Toggle bleibt (true ↔ false). Cascade automatisch im Backend.
+- Kein "Reset to inherit"-UI — User toggled stattdessen den Parent erneut.
+
+## Schritte
+
+### S1: Datenmodell + Migration
+- `datamodelDataSource.py` und `datamodelFeatureDataSource.py`: `Optional[bool]`/`Optional[str]` für die drei Flags
+- Migration-Skript `script_db_migrate_datasource_inherit.py` (additiv-idempotent): ALTER COLUMN für die Spalten zu nullable. Bestehende Werte bleiben.
+
+### S2: Cascade-Helper im Backend
+- Neue Datei `gateway/modules/serviceCenter/services/serviceUdb/_cascadeInherit.py`:
+ - `cascadeResetDescendants(rootIf, dataSourceId, flag) -> int` — gibt Anzahl betroffener Records zurück
+- Audit-Logging via `recordModify` mit Reason `cascade_reset_`
+
+### S3: PATCH-Endpoints anpassen
+- `routeDataSources.py`: `_updateDataSourceNeutralize`, `_updateDataSourceRagIndex`, `_updateDataSourceScope`
+- Body akzeptiert `value: Optional[bool|str]`
+- Bei nicht-null Wert: führe `cascadeResetDescendants` aus
+- Bei null Wert: nur setzen, kein Cascade
+
+### S4: Walker-Integration
+- `serviceKnowledge/_effectiveFlags.py` (neu): `getEffectiveFlag(ds, flag, allDsByConnection)`, `getAncestorChain(ds, allDs)`
+- Walker (RAG-Sync, Neutralisierungs-Pipeline) nutzen den Helper statt direkten `ds.neutralize`-Zugriffs
+- `_loadRagEnabledDataSources`: filtert nach **effektivem** `ragIndexEnabled` (statt direktem)
+
+### S5: Frontend
+- `connectionApi.ts`: `UdbDataSource` Interface auf `Optional`-Werte
+- `SourcesTab.tsx`: `_effectiveValue` Helper, `_findParentDs` Helper
+- `_TreeNodeView`: `effectiveValue` via Helper, nicht direkt `ds.neutralize ?? inheritedNeutralize`
+- Toggle-Handler unverändert: PATCH macht Cascade automatisch
+- Nach PATCH: `_fetchDataSources()` damit Cascade-Reset im UI sichtbar
+
+### S6: Tests
+- `test_cascade_inherit.py`: Cascade-Reset löscht nur das Flag, nicht andere Flags
+- `test_effectiveFlag.py`: Path-traversal returns Root-Default wenn alle Ancestors NULL sind
+- Bestehende RAG-Bootstrap-Tests: `ragIndexEnabled` Werte anpassen (explicit true statt implicit)
+
+### S7: Walker-Smoke
+- Manuell: Connection mit Sub-Folders, Toggle Parent → Sub-Folder folgen ohne Cascade-DELETE
+
+### S8: Wiki + Changelog
+- `b-reference/gateway/architecture.md`: neuer Abschnitt "DataSource Cascade-Inherit"
+- `_CHANGELOG.md` mit Begründung
+
+## Risiken
+
+- **Breaking Change** für DataSource-DTO: Frontend muss `null` korrekt handeln. Mitigation: TypeScript-Types eng machen, alle Lese-Stellen prüfen.
+- **Walker-Performance**: Path-Traversal pro Item könnte O(N×depth) werden. Mitigation: Pre-Computed Map `path → effectiveFlag` vor Walker-Run.
+- **Migrations-Race**: bestehende Records bleiben mit ihren Werten. Falls ein User einen DS auf `false` explizit gesetzt hat (in der alten Welt = "false default"), bleibt es nach Migration `false` explizit, nicht `NULL`. Das ist OK — User-Wille bleibt respektiert.
+
+## Out of Scope
+
+- 3-state UI (inherit/true/false) auf einem einzigen Toggle-Button — bleibt 2-state
+- Settings-Modal "Reset to inherit"-Button — explizit verworfen vom User
+- FeatureDataSource Cascade-Tests — analog zu DataSource, separater Folge-PR
diff --git a/c-work/2-build/2026-05-udb-datasource-settings.md b/c-work/2-build/2026-05-udb-datasource-settings.md
new file mode 100644
index 0000000..5e2eb52
--- /dev/null
+++ b/c-work/2-build/2026-05-udb-datasource-settings.md
@@ -0,0 +1,187 @@
+
+
+
+
+
+# UDB DataSource Settings (⚙️) + konfigurierbare RAG-Limits
+
+> **Implementierungsstand 2026-05-17:** S0–S11 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
+
+```
+
+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
+... Limit {l} erreicht. Weitere Dateien wurden NICHT indexiert. {' '}
+ openSettingsModalForConn(conn)}>Limit anpassen oder
+ DataSource enger eingrenzen, dann erneut starten.
+```
+
+## 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.
diff --git a/c-work/3-validate/2026-05-feature-data-agent-ontology-and-repair.md b/c-work/4-done/2026-05-feature-data-agent-ontology-and-repair.md
similarity index 100%
rename from c-work/3-validate/2026-05-feature-data-agent-ontology-and-repair.md
rename to c-work/4-done/2026-05-feature-data-agent-ontology-and-repair.md
diff --git a/c-work/_CHANGELOG.md b/c-work/_CHANGELOG.md
index 057b2d8..c00d1db 100644
--- a/c-work/_CHANGELOG.md
+++ b/c-work/_CHANGELOG.md
@@ -14,6 +14,10 @@ Skip: reine Refactors, Formatting, Lint, Dep-Bumps, Test-only, Wiki-Tippfehler.
## 2026-05-17
+- 2026-05-17 | fix | frontend-nyla | **UDB Settings-Modal: RAG-Limits nur auf DataSource-Root** — Settings-Icon (⚙️) bleibt auf allen Nodes sichtbar, aber RAG-Limits- und Kostenschätzungs-Sektionen werden nur noch auf DataSource-Root-Nodes (Level 2 = `service`) angezeigt. Subelemente (Folder/File) können weiterhin die Connection-Settings sehen, erben aber die Walker-Limits vom Root. Neue Modal-Prop `showRagSection`. Neutralisierung/RAG-Toggle: Vererbungslogik ist korrekt (Parent aktiviert → Kinder werden mitgezogen, volle Opacity). Kein visueller Unterschied nötig — das ist gewolltes Verhalten.
+- 2026-05-17 | feat | gateway+frontend-nyla | **i18n für BackgroundJob-Progress-Messages (Backend-translated)** — User-Report: RAG-Page zeigte "145 Dateien verarbeitet, 106 indexiert" auch bei UI-Sprache=`en`, weil walker das Plaintext-Deutsch direkt in `BackgroundJob.progressMessage` schrieben und das Frontend es 1:1 rendert. Root-Cause: BackgroundWorker hat keinen Request-Sprach-Kontext (`_CURRENT_LANGUAGE` ist ContextVar pro Request), und `progressMessage` wird persistiert — wäre selbst dann gefroren, wenn der User später die Sprache wechselt. **Architektur (regelkonform zu `wiki/b-reference/gateway/architecture.md#i18n`):** Backend speichert strukturiert + übersetzt server-side beim Route-Read; Frontend rendert 1:1 — kein `t()` auf Backend-Werten. (1) Neue JSONB-Spalte `BackgroundJob.progressMessageData = {key, params}` (Migration `script_db_migrate_backgroundjob_progress_data.py`, additiv + idempotent). (2) `JobProgressCallback.__call__` akzeptiert `messageKey="LITERAL"` + `messageParams={…}` und schreibt beides als JSON; zusätzlich rendert es einen DE-Fallback in `progressMessage` für Logs/Audit/Legacy-Clients. (3) Alle Walker (6 RAG + `subConnectorIngestConsumer` + Trustee push/sync/import + `accountingDataSync._progress`) umgestellt — `messageKey=` ist immer ein String-Literal. (4) Key-Registrierung über string-literale `t("…")` Calls: neues `serviceKnowledge/_progressMessages.py` (Side-Effect-Import in `app.py` lifespan, 5 RAG-Keys), Trustee 14 Keys in `mainTrustee.py` — KEINE Variable-Aufrufe von `t()` (Wiki-Regel #1: `t(variable)` ist verboten). (5) Neuer Helper `i18nRegistry.resolveJobMessage(messageData)` analog zu `resolveText(value)` — der einzige zulässige `t(variable)`-Pfad, weil er in der i18n-Infrastruktur lebt; nutzt `_CURRENT_LANGUAGE` aus dem Request-Kontext und substituiert Params via `.format(**params)`. (6) `routeJobs._serialiseJob` und `routeRagInventory` rufen `resolveJobMessage` beim Read und schreiben das Ergebnis in `progressMessage` — Frontend bekommt einen fertigen, übersetzten String. (7) Frontend zurückgebaut: `utils/jobProgressUtils.ts` Helper **gelöscht**, DTOs (`useBackgroundJob`, `connectionApi`, `trusteeApi`) ohne `progressMessageData`-Feld, Render-Stellen (`RagInventoryPage`, `RagRunningBadge`, `TrusteeAccountingSettingsView`) lesen direkt `job.progressMessage`. Tests: 22/26 grün; die 4 Failures in `test_knowledge_ingest_consumer.py` sind pre-existing (verifiziert via `git stash` Diff). Frontend `npm run build` grün. Smoke: `resolveJobMessage({'key': '{n} Dateien verarbeitet, {indexed} indexiert', 'params': {'n': 145, 'indexed': 106}})` → `'145 Dateien verarbeitet, 106 indexiert'`. Wiki: neuer Abschnitt "BackgroundJob-Progress-Messages" in `b-reference/gateway/architecture.md` mit den 4 Schritten (Walker → Registrierung → Route-Resolve → Frontend-Render).
+- 2026-05-17 | feat | gateway+frontend-nyla | **UDB DataSource Settings (⚙️) + konfigurierbare RAG-Limits** (Plan & Build: `c-work/2-build/2026-05-udb-datasource-settings.md`). Schliesst zwei Lücken: (1) RAG-Walker-Limits (`maxBytes=200 MB` etc.) waren hartkodiert — User mit 500-MB-Folder konnte nur Code-Änderung machen; (2) FeatureDataSource hatte gar keinen Settings-Ort. **Backend**: JSONB-Spalte `settings` auf `DataSource` + `FeatureDataSource` (Migration `script_db_migrate_datasource_settings.py`, additiv + idempotent). Neues Modul `serviceKnowledge/_ragLimits.py` mit `FILES_LIMITS_DEFAULT` / `CLICKUP_LIMITS_DEFAULT` als zentrale Source-of-Truth — die alten `MAX_*_DEFAULT`-Konstanten in den 4 Walkern (`subConnectorSyncSharepoint/Kdrive/Gdrive/Clickup.py`) sind nur noch Aliase. Kritische Semantik: `getStoredOverrides(ds, kind)` liefert NUR explizit gesetzte Overrides → Walker mergen sie auf den **caller-supplied** `limits=`-Parameter, damit Test-/Caller-Overrides weiter gewinnen (`test_bootstrap_maxTasks_caps_ingestion=3` bleibt grün); `getRagLimits(ds, kind)` mergt auf Defaults → API/Cost-Estimate-Pfad. **Keine Override-Schicht, keine Resolver-Logik** — was im Modal steht, ist exakt was der Walker liest. Zwei neue Endpunkte in `routeDataSources.py`: `PATCH /api/datasources/{id}/settings` (akzeptiert nur Top-Level-Key `ragLimits`, unknown → 400, positive Ints only, Owner-only/Mandate-Admin, Audit-Log `datasource_settings_changed`) und `GET /api/datasources/{id}/cost-estimate` (indikative USD-Schätzung via `_costEstimate.py`-Heuristik: `text-embedding-3-small @ $0.02/1M Token`, `BYTES_PER_TOKEN=4`, `EXTRACTABLE_FRACTION=0.4`; Antwort trägt vollständiges `basis`-Objekt mit Annahmen/Formel/Notes). **Frontend**: Neues ⚙️-Icon pro Node im `UnifiedDataBar/SourcesTab.tsx` (vor dem 🧠) öffnet den neuen `DataSourceSettingsModal.tsx` mit drei klar abgegrenzten Sektionen: (1) **Connection** — `knowledgeIngestionEnabled`-Toggle via `patchKnowledgeConsent` (mit Confirm-Dialog beim Deaktivieren); (2) **RAG-Limits** — Felder editierbar, Bytes in MB im UI; (3) **Kostenschätzung** — refresh nach Save. Dasselbe Modal wird auf der `RagInventoryPage.tsx` vom amber Partial-Banner (`stoppedAtLimit`) via neuen "Limit anpassen"-Button geöffnet → User hat direkten Pfad vom Symptom zur Behebung. Workspace-Route `GET /api/workspace/{instanceId}/connections` liefert jetzt `knowledgeIngestionEnabled` mit, damit der Modal-Initial-Toggle korrekt vorbelegt. **Tests**: 12 neue Unit-Tests in `tests/unit/services/test_ragLimits.py` + 6 in `test_costEstimate.py` (Defaults-Isolation, partial-override, non-int dropped, doubling-formula, basis-shape) — alle grün; bestehende Bootstrap-Tests für Sharepoint/Kdrive/Gdrive/ClickUp weiter grün (caller-limits-Override respektiert). Frontend `npm run build` grün, keine neuen Lint-Errors. Doku: `b-reference/gateway/ai-agent.md` (Abschnitt "Konfigurierbare RAG-Limits"), `TOPICS.md` (neuer Eintrag). Verbleibende Hard-Limits in `subConnectorSyncOutlook/Gmail.py` haben aktuell kein UI-Override, bleiben aber als next-step (gleicher Helper anwendbar).
+- 2026-05-17 | feat | frontend-nyla+gateway | **Knowledge-Consent Toggle auf ConnectionsPage + Forward-Sync DataSource→Connection.** Zwei Lücken in der RAG-Consent-UX geschlossen, die zu der Beobachtung "valueon hat Index aktiviert, aber Checkbox fehlt" geführt haben: (1) Die ConnectionsPage zeigte `knowledgeIngestionEnabled` nur als generische "Ja/Nein"-Spalte der FormGeneratorTable — kein Toggle-Element. Neu: zwei CustomActions (`FaToggleOn`/`FaToggleOff`, je nach State sichtbar via `visible`-Filter), Klick ruft `patchKnowledgeConsent` → `/api/connections/{id}/knowledge-consent` und refetcht die Liste. Damit ist die UI 1:1 konsistent mit dem Master-Switch auf der RagInventoryPage (gleiches Backend-Endpoint, gleiches Icon, gleicher Confirm-Dialog beim Deaktivieren). (2) Backend: `routeDataSources._updateDataSourceRagIndex` propagierte bisher nicht auf die Parent-Connection. Neuer Helper `_ensureConnectionKnowledgeFlag(rootIf, connectionId)` setzt **forward-only** `UserConnection.knowledgeIngestionEnabled=True`, sobald min. eine DataSource auf `ragIndexEnabled=true` toggelt — kein Auto-Disable, weil der Master-Switch dem User gehört (verhindert versehentliches Zurücksetzen eines explizit gegebenen Consents, z.B. einer Connection ohne aktive DataSource, aber mit `knowledgePreferences`). Plan-Doc für die UDB-Settings-Erweiterung (Issue 2): `c-work/1-plan/2026-05-udb-datasource-settings.md`. Der Fix für Limit-Transparenz wirkte UI-seitig nicht, weil `_bootstrapJobHandler` in `subConnectorIngestConsumer.py` die Sub-Service-Results in ein wrappendes Dict packt (`{"authority", "connectionId", "sharepoint": {...}, "outlook": {...}}` für msft; analog für `drive`/`gmail`/`clickup`/`kdrive`). `routeRagInventory._buildConnectionInventory` griff aber auf Top-Level `result.stoppedAtLimit`/`indexed`/etc. zu — alle `None`. Folge: amber Banner blieb aus UND die Statistik-Zeile zeigte gar keine Zahlen ("Sync erfolgreich" ohne "— 25 unverändert"). Neuer `_flattenJobResult()`-Helper aggregiert über alle bekannten Sub-Keys (sum für counters, max für durationMs, erstes Limit-Hit für `stoppedAtLimit`/`limits`). Verifiziert anhand Job `2374aecd-3e17-460a-a13e-530f9f1115e6`: `bytesProcessed=209894527` ≥ `maxBytes=209715200`, jetzt korrekt als `stoppedAtLimit="maxBytes"` an die UI durchgereicht. Diagnose-Skript `gateway/scripts/debug_rag_job_result.py` zeigt vor/nach-Flatten und bleibt für künftige Bootstrap-Result-Debugging im Repo.
- 2026-05-17 | feat | gateway+frontend-nyla | **RAG-Inventar: echte Chunks + Limit-Transparenz.** Drei Probleme behoben: (1) `routeRagInventory._buildConnectionInventory` zählte bisher `len(FileContentIndex)` (= indizierte Dateien) und labelte das im UI als "Chunks" — bei einer 99-Seiten-PDF erscheint dort statt der echten ~99 Chunks die Zahl 1. Neue `interfaceDbKnowledge.countChunksByFileIds()` macht eine einzige Aggregat-SQL `SELECT "fileId", COUNT(*) FROM "ContentChunk" WHERE "fileId" = ANY(%s) GROUP BY "fileId"` (kein Vector-Body geladen), die Response trägt jetzt `fileCount` UND `chunkCount` pro DataSource + `totalFiles/totalChunks` pro Connection. (2) `RagInventoryPage.tsx` / `connectionApi.ts` zeigen beide Werte getrennt ("25 Dateien · 1240 Chunks") mit Tooltip-Definition für Chunks (~400 Tokens). (3) **Limit-Transparenz**: SharePoint/kDrive/gDrive-Bootstrap stoppen bei den ersten Limits (`MAX_BYTES_DEFAULT=200 MB`, `MAX_ITEMS_DEFAULT=500`, `MAX_DEPTH_DEFAULT=4`, `MAX_FILE_SIZE_DEFAULT=25 MB`); ClickUp analog (`MAX_TASKS_DEFAULT=500`, `MAX_WORKSPACES_DEFAULT=3`, `MAX_LISTS_PER_WORKSPACE_DEFAULT=20`). Bisher: `return` ohne Log + ohne Marker im Bootstrap-Result → User sah "Sync erfolgreich" obwohl 706 Dateien fehlten. Fix: neuer `_recordLimitStop()`-Helper in allen 4 Connectoren setzt `BootstrapResult.stoppedAtLimit` (1. exhausted Budget), schreibt 1 WARNING in den Log und liefert das Feld + die effektiven `limits` im `_finalizeResult` Dict an `BackgroundJob.result`. `routeRagInventory` reicht `lastSuccess.stoppedAtLimit/limits/bytesProcessed` ans Frontend durch. Neuer amber `partialBanner` auf der RagInventoryPage warnt mit "Limit maxBytes=200 MB (200 MB verarbeitet) erreicht — Weitere Dateien wurden NICHT indexiert" und bietet "Erneut indexieren". Verifiziert anhand `local/logs/log_app_20260517.log`: SharePoint-Sync hat genau bei `bytesProcessed=209_894_527 ≥ MAX_BYTES_DEFAULT (209_715_200)` gestoppt (Kumulative Summe der 25 indizierten Dateigrößen = 200.17 MB). ClickUp hat bei `skippedDup=500 >= maxTasks=500` gestoppt. Outlook/Gmail brauchen das gleiche Pattern noch (haben aktuell keine harten Limits im Code, daher kein Bug, aber wenn welche kommen → gleicher Helper).
- 2026-05-17 | fix | gateway | **Secrets Decryption TTL-Cache** (`gateway/modules/shared/configuration.py`): `decryptValue()` cached jetzt erfolgreich entschlüsselte Plaintexts process-wide für 60 s (Key = Ciphertext, thread-safe, `clearDecryptionCache()` für Rotation/Tests). Root-Cause aus S7-Smoke-Test (`local/logs/log_app_20260517.log:609`): RAG-Inventory-Polling + paralleler Walker-Burst triggerte für `system`/`DB_PASSWORD_SECRET` >10 Decrypts/s, das Brute-Force-Schutz-Rate-Limit warf `ValueError: Decryption rate limit exceeded` → `routeRagInventory._getInventoryPlatform` HTTP 500. Hot-Path war `mainBackgroundJobService._getDb()`, das pro Call `APP_CONFIG.get("DB_PASSWORD_SECRET")` evaluiert (eager arg eval), bevor `getCachedConnector` überhaupt seinen Wrapper-Cache prüfen kann. Cache-Hit umgeht das Rate-Limit (kein neuer Krypto-Op, nur Re-Read eines bereits autorisierten Plaintexts); Cache-Miss konsumiert weiter Rate-Budget — die Schutzfunktion gegen wiederholt falsche Decrypts bleibt damit erhalten. Wirkt global für alle `_SECRET`-Reader (`auditLogger`, `routeI18n`, alle Feature-Interfaces), nicht nur für den BackgroundJobService.
- 2026-05-17 | refactor | gateway | PostgreSQL Connection Pool — Steps S3–S6 abgeschlossen (`c-work/2-build/2026-05-postgres-connection-pool.md`). **S3**: `getCachedConnector` Docstring präzisiert (Cache = Wrapper-Recycling + DB-Init-Spam-Schutz, Pool = echte Connection-Verwaltung). **S4**: Shutdown-Hook `closeAllPools()` in `gateway/app.py` lifespan als letzter Schritt nach Feature-`onStop`-Hooks. **S5**: Neuer Test-File `gateway/tests/unit/connectors/test_connectorDbPostgre_pool.py` mit 6 Concurrency-Tests gegen live-Postgres (auto-skip wenn keine DB erreichbar): 50 Threads × 20 Reads (0 Errors), 20 Threads × 50 Reads (p99 < 5 s), interleaved load/save, `statement_timeout=500ms` triggert `QueryCanceled` und gibt Connection sauber zurück, Pool-Identity pro (host, db, port), `closeAllPools` leert Registry. Beim ersten Lauf entdeckt: psycopg2-Pool wirft `PoolError` sofort bei Exhaustion statt zu blockieren → `borrowConn()` um bounded Wait-Retry erweitert (`_BORROW_WAIT_TIMEOUT_S=30s`, `_BORROW_WAIT_BACKOFF_S=50ms`). Alter `test_connectorDbPostgre_failLoud.py` auf das neue `borrowConn`-Mocking umgestellt (alle 6 weiter grün). **S6**: Regression-Run: 639/656 unit grün (vorher 638) — der eine durch den Refactor verursachte Fail (`test_folder_crud._FakeDb` brauchte `borrowCursor`-Stub) gefixt, die übrigen 17 Failures sind pre-existing RAG/Adapter/Workflow-Drift ohne Pool-Bezug. 76/79 integration grün (3 pre-existing Trustee-Workflow-Fails). Backward-Compat-Stub `borrowCursor` auch in `test_folderRbac._FakeDb` ergänzt. Offen: **S7** (manueller 1 h Smoke-Test, Anleitung in der Plan-Doc) und **S8** (`b-reference/platform/database-architecture.md`).