data source flag cascading issue

This commit is contained in:
ValueOn AG 2026-05-18 00:26:13 +02:00
parent 656cf3eacb
commit 57579141cf
7 changed files with 411 additions and 4 deletions

View file

@ -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 |

View file

@ -1,6 +1,6 @@
<!-- status: canonical -->
<!-- lastReviewed: 2026-05-15 -->
<!-- verifiedAgainst: gateway (codebase audit 2026-04-07, post Automation Unification); gateway/modules/features/teamsbot/service.py (Hybrid Agent Escalation 2026-04-24); Typed Action Architecture Phasen 1-5; featureDataAgent domain hints hook 2026-04-27; central parameterValidation + DatabaseQueryError 2026-04-28; OpenAI temperature contract for GPT-5.x / o-series 2026-04-28; Voice STT speechToText params note 2026-05-10; RAG Consent & Control Unification (Phases A-D) 2026-05-12; Zombie-Killer + Walker-Timeouts 2026-05-14; FeatureDataAgent Query-Repair-Loop + Ontology layer 2026-05-15 -->
<!-- lastReviewed: 2026-05-17 -->
<!-- verifiedAgainst: gateway (codebase audit 2026-04-07, post Automation Unification); gateway/modules/features/teamsbot/service.py (Hybrid Agent Escalation 2026-04-24); Typed Action Architecture Phasen 1-5; featureDataAgent domain hints hook 2026-04-27; central parameterValidation + DatabaseQueryError 2026-04-28; OpenAI temperature contract for GPT-5.x / o-series 2026-04-28; Voice STT speechToText params note 2026-05-10; RAG Consent & Control Unification (Phases A-D) 2026-05-12; Zombie-Killer + Walker-Timeouts 2026-05-14; FeatureDataAgent Query-Repair-Loop + Ontology layer 2026-05-15; UDB DataSource Settings + configurable RAG-Limits 2026-05-17 -->
# 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.<key>` (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)

View file

@ -1,6 +1,6 @@
<!-- status: canonical -->
<!-- lastReviewed: 2026-05-10 -->
<!-- verifiedAgainst: gateway (codebase audit 2026-04-07, post Automation Unification) + Typed Action Architecture Phasen 1-5 + Infomaniak-Connector (2026-04-28) + MSFT/Google Calendar+Contacts + Reconnect (2026-04-28) + Google Voice STT/TTS (2026-05-10) -->
<!-- lastReviewed: 2026-05-17 -->
<!-- verifiedAgainst: gateway (codebase audit 2026-04-07, post Automation Unification) + Typed Action Architecture Phasen 1-5 + Infomaniak-Connector (2026-04-28) + MSFT/Google Calendar+Contacts + Reconnect (2026-04-28) + Google Voice STT/TTS (2026-05-10) + BackgroundJob i18n payload (2026-05-17) -->
# 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
<span>{conn.runningJobs[0].progressMessage || t('Synchronisierung läuft...')}</span>
```
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:

View file

@ -0,0 +1,150 @@
<!-- status: plan -->
<!-- started: 2026-05-18 -->
<!-- component: gateway | frontend-nyla -->
# 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=<service>`.
### 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_<flag>`
### 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

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

@ -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 S3S6 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`).