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