wiki/c-work/4-done/2026-05-rag-consent-and-control-unification.md
2026-05-16 22:54:27 +02:00

363 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- status: done -->
<!-- started: 2026-05-12 -->
<!-- lastReviewed: 2026-05-15 -->
<!-- component: gateway | frontend-nyla | platform -->
# Unified RAG Consent, Neutralization and Visibility — Single Source of Truth
## Beschreibung und Kontext
Mit dem Plan **`2026-04-id-unified-knowledge-indexing-rag-concept.md`** (P0P1d) wurde der **technische Backend-Pfad** für RAG-Ingestion implementiert (Connection-OAuth → Bootstrap → Walker → `requestIngestion``interfaceDbKnowledge`) und der **`AddConnectionWizard`** ausgeliefert (Consent + Preferences + Kostenabschätzung).
Bei der Architektur-Review (2026-05-12) wurde jedoch festgestellt:
- **Wizard ist standalone**, ohne geschlossenen Lebenszyklus für seine Setzungen.
- **Consent ohne Revoke-UI** — User kann „Wissensdatenbank: Ja" wählen, aber nirgends widerrufen ohne die Connection zu löschen.
- **Doppelte Neutralisierungs-Quelle:** `UserConnection.knowledgePreferences.neutralizeBeforeEmbed` (global pro Connection, gesetzt im Wizard) **und** `DataSource.neutralize` (UDB, pro Pfad). Beide wirken unabhängig — verwirrt den User.
- **Surface Toggles und MIME-Allowlist sind tot:** Werden geparst, aber in keinem Walker referenziert.
- **`GET /api/connections/` zeigt Knowledge-Felder nicht.** Selbst wenn UI gebaut wäre, fehlt die Datenquelle.
- **Bootstrap-Job-Status für User unsichtbar:** Bootstrap-Jobs ohne `mandateId``/api/jobs` blockt. User klickt „Verbinden" und sieht **kein Feedback**.
- **Indexierung kann nicht gestoppt werden:** Wenn ein Bootstrap einer 50'000-Item-Mailbox losläuft, gibt es keinen Stop-Button. Kein Cost-Cap.
- **`WorkspaceRagInsightsPage` am falschen Ort:** Workspace-instance-scoped Stats für ein plattformweites Konzept (`page.system.ragInventory` wäre richtig).
- **„Was ist neu zu indexieren" ist undurchsichtig:** Walker walked alles, dedupliziert via Content-Hash. Es ist korrekt, aber für den User nirgends erklärt.
- **Walker-Scope ist OAuth-global**, nicht user-gesteuert. User kann nicht wählen „SharePoint-Site A ja, Site B nein".
**Business-Treiber:** DSGVO-Compliance (Art. 7: Widerruf so einfach wie Erteilung), Kosten-Kontrolle (Stop-fähig, scope-fähig), und Vertrauen (Transparenz: was ist drin, wann reinprozessiert, wie raus).
## Fokus und kritische Details
### Architektonisches Leitprinzip (vom User formuliert)
> „Der Wizard ist nur eine UI-Komponente. Dies ist keine Architektur. Die Architektur muss im Backend datenzentriert sein und jeder Schritt muss transparent sein und im UI für den User ersichtlich."
Daraus ergeben sich **vier harte Regeln**:
1. **Jede Einwilligung im Wizard MUSS an genau einem Ort im UI revozierbar sein.** „Knowledge Ingestion" `on/off` muss in `ConnectionsPage` und auf der RAG-Inventar-Seite umstellbar sein, mit demselben Backend-Effekt wie Connection-Revoke (Purge).
2. **Neutralisierung wird AUSSCHLIESSLICH in der UDB pro Tree-Element verwaltet.** Es darf keine „Neutralisierung global einer Datenquelle" geben, die „einfach so" wirkt. `UserConnection.knowledgePreferences.neutralizeBeforeEmbed` ist Anti-Pattern und wird **entfernt**.
3. **Was im RAG ist, muss der User sehen UND auf der Granularität entfernen können, auf der er es hinzugefügt hat.**
4. **Indexierung muss jederzeit stoppbar sein** — sowohl als „Stopp-Knopf" auf laufenden Jobs als auch als impliziter Stop durch das Entfernen des Index-Toggles in der UDB.
### Kern-Idee: Per-Tree-Element-Toggle (analog zu Scope und Neutralize)
**Heute** in der UDB hat jedes Tree-Element drei Aktionen-Buttons: 💬 Chat / 🧑 Scope-Cycle / 🔒 Neutralize-Toggle. Die `DataSource`-Row materialisiert sich, sobald der User **eine** dieser Aktionen für einen Pfad konfiguriert (`scope`, `neutralize`).
**Morgen** kommt ein **vierter** Button hinzu: 🧠 **RAG-Index-Toggle**. Selbe Mechanik:
- Toggle aktivieren → DataSource bekommt `ragIndexEnabled = true` → wird beim nächsten Bootstrap/Resync indexiert
- Toggle deaktivieren → `ragIndexEnabled = false` + sofortiger Purge-Job für diesen Pfad
- Damit ist der Index-Lebenszyklus **strukturidentisch** mit Scope und Neutralize. Konsistent und intuitiv.
**Konsequenz:** Walker iterieren **nicht** mehr OAuth-global. Sie iterieren über **`DataSource`-Rows mit `ragIndexEnabled = true`** der jeweiligen Connection. Der User entscheidet **explizit pro Tree-Element** was indexiert wird.
#### Granularität: Tree-Kaskade vs. Tabellen-Felder
Die UDB unterscheidet schon heute zwei Daten-Typologien — beide Policies (`neutralize`, `scope`, künftig `ragIndexEnabled`) müssen sich konsistent dazu verhalten:
| Daten-Typ | Beispiele | Wie wirken Policies |
|-----------|-----------|---------------------|
| **Tree (`DataSource`)** | SharePoint-Folder, Outlook-Mail-Folder, Drive-Folder, ClickUp-Liste | **Kaskadierend**: Eine Policy auf einem Container-Knoten (Folder/Site) **vererbt** sich auf alle Kinder, bis ein Kind eine eigene Policy explizit überschreibt. Code-Pattern existiert bereits (`inheritedScope`, `inheritedNeutralize` in `SourcesTab.tsx`). `ragIndexEnabled` muss **denselben** Vererbungs-Mechanismus nutzen — sonst inkonsistent. |
| **Tabular (`FeatureDataSource`)** | Trustee-Positionen-Tabelle, RealEstate-Bauvorschriften, beliebige Feature-DATA_OBJECT-Tabellen | **Pro Feld**: `FeatureDataSource.neutralizeFields: List[str]` — User kann sagen „indexier diese Tabelle, aber `email`-Feld neutralisieren, `name`-Feld lassen". Bestehender Code: `_toggleNeutralizeField` in `SourcesTab.tsx`. Für `ragIndexEnabled` auf Tabellen-Ebene ist Per-Feld-Granularität **nicht** sinnvoll (entweder Zeile geht in RAG oder nicht) — also: `ragIndexEnabled` gilt **pro Tabelle/`FeatureDataSource`**, **nicht** pro Feld. |
**Verbindliche Regeln:**
1. **`neutralize` auf Container vererbt sich nach unten**, bis ein Child-Knoten einen eigenen `neutralize`-Wert hat (true oder false). „Inherited"-Visualisierung im UI bleibt wie heute (gestrichelt / gedimmt).
2. **`ragIndexEnabled` auf Container vererbt sich gleich**: Toggle auf einem SharePoint-Site-Knoten = alle Subfolders+Files werden indexiert (mit der vererbten `neutralize`-Policy), ausser ein Subfolder-Knoten setzt explizit `ragIndexEnabled = false`.
3. **Tabellarisch (`FeatureDataSource`)**: `neutralizeFields: List[str]` ist die einzige Granularitäts-Stufe für Neutralisierung. `ragIndexEnabled` gilt **pro Tabelle**. Wenn der User bestimmte Spalten **gar nicht** im RAG haben will, ist die Lösung „Tabelle nicht indexieren" oder ein neuer `excludeFields: List[str]` (Roadmap-Item, **nicht** in v1).
4. **Walker-Implementierung muss die Vererbung respektieren**: Wenn User SharePoint-Site indiziert (Container) und nicht jeden Subfolder einzeln, durchläuft der Walker die Tree-Hierarchie und nutzt für jedes File die **effektive** Policy (eigene > geerbt > default `false`).
**Vorteile:**
- Granularität: User wählt „SharePoint Site A → Folder 'Reports' indexieren, Folder 'Drafts' nicht".
- Revoke trivial: Ein Klick auf den Toggle in der UDB → automatischer Purge.
- Einheitliche UX-Pattern (gleiche Iconisierung wie Scope/Neutralize).
- Kein „Auto-Default-DataSources beim Connect"-Hack — User muss aktiv was wählen, was DSGVO-konformer ist (kein Default-Indexing ohne explizite Wahl).
- **DataSource braucht kein separates `autoSync`/`autoIndex`-Attribut auf Row-Level** — die `ragIndexEnabled`-Spalte IST die Speicherung; das Icon IST die Bedienung.
**Konsequenz für `knowledgeIngestionEnabled` auf der Connection:**
- **Bleibt** als rechtlicher Master-Schalter („darf PowerOn überhaupt etwas aus dieser Connection in die Wissensdatenbank legen?").
- Wenn `false`: Kein Walker startet, egal welche `ragIndexEnabled`-DataSources existieren. Existierende Chunks werden gepurged.
- Wenn `true`: Walker iteriert über DataSources mit `ragIndexEnabled = true`. Wenn keine existiert → Walker macht nichts (kein leerer Default-Walk).
### Indexierung jederzeit stoppbar
Drei Stop-Mechanismen:
1. **Implizit pro Element:** In der UDB den Index-Toggle einer DataSource ausschalten → Purge dieser Items + Walker überspringt sie beim nächsten Lauf.
2. **Implizit pro Connection:** Master-Toggle auf der Connection ausschalten (Knowledge-Consent revoke) → Purge **aller** Chunks dieser Connection + nächste Bootstrap-/Resync-Jobs werden gar nicht erst gestartet.
3. **Explizit pro laufendem Job:** **Stop-Button** in der RAG-Inventar-Seite und auf der ConnectionsPage, sobald ein Bootstrap-Job `RUNNING` ist. Backend setzt Job-Status auf `CANCELLED`; Walker prüft alle ~50 Items das Cancel-Flag und beendet graceful.
- Bereits indexierte Items bleiben drin (kein Rollback). Wer komplett raus will → Master-Toggle off.
- `BackgroundJobStatusEnum.CANCELLED` existiert bereits im Enum — fehlt nur die `cancelJob()`-API + Cancel-Check im Walker-Loop.
### „Was wird neu indexiert?" — Dedup-Mechanismus transparent gemacht
Die Frage taucht beim User auf: „Wenn ich in 24h meinen täglichen Resync laufen lasse — wie weiss das System was neu ist?"
Die Antwort existiert technisch **bereits**, ist aber nirgends im UI sichtbar gemacht:
| Ebene | Mechanismus | Im Code |
|-------|-------------|---------|
| **Pro Item, vor API-Call** (Cheap-Skip) | Walker speichert externen Revisions-Token (`eTag` für SharePoint, `modifiedTime` für Drive, `historyId` für Gmail, `changeKey` für Outlook, `date_updated` für ClickUp). Wenn unverändert → Walker skippt den API-Download. | Bootstrap-Walker, Provider-Adapter |
| **Pro Item, nach Extraction** (Embedding-Skip) | `requestIngestion` baut Content-Hash über `(contentType, data, extractor-order)` — Pure Function des Inhalts. Wenn `FileContentIndex.structure._ingestion.hash` matcht → Status bleibt `indexed`, kein neues Embedding. | `mainServiceKnowledge.requestIngestion` |
| **Bei Connection-Revoke** | `deleteFileContentIndexByConnectionId` purged alle Rows mit `connectionId = X`. | `interfaceDbKnowledge` |
| **Bei DataSource-Revoke** (NEU in diesem Plan) | `deleteFileContentIndexByConnectionAndPathPrefix` purged Rows mit `connectionId = X` und `contextRef.path` startet mit Y. | NEU |
Daraus folgt für den Plan:
- **Keine neue Mechanik nötig** für „was ist neu" — die Hash-Idempotenz reicht.
- **UI-Aufgabe:** Auf der RAG-Inventar-Seite muss klar dokumentiert/sichtbar sein: „Letzter Sync: vor 18h. Neue Items werden täglich um 02:00 ergänzt. Unveränderte Items werden nicht erneut verarbeitet."
- **Pro Sync sichtbare Counter:** `indexedNew`, `skippedDuplicate`, `failed` (existieren als Log-Events; müssen pro Connection auf der Inventar-Seite aggregiert werden).
### Konsequenzen für die Datenmodellierung
| Heute | Wird zu |
|-------|--------|
| `UserConnection.knowledgeIngestionEnabled` | **bleibt** — rechtlicher Master-Gate, mit Revoke-UI |
| `UserConnection.knowledgePreferences.neutralizeBeforeEmbed` | **entfernen** — Migration: bei `true` werden alle existierenden DataSources der Connection auf `neutralize=true` gesetzt |
| `UserConnection.knowledgePreferences.{mailContentDepth, mailIndexAttachments, filesIndexBinaries, clickupScope, maxAgeDays}` | **bleiben** als Connection-weite Default-Sync-Einstellungen, Edit-UI in Connection-Detail-Drawer |
| `UserConnection.knowledgePreferences.{mimeAllowlist, surfaceToggles}` | **entfernen** — durch Per-Element-Toggle obsolet |
| `DataSource.neutralize` | **bleibt** — einzige Quelle für Neutralisierungs-Policy pro Tree-Element |
| `DataSource.scope` | **bleibt unverändert** |
| `DataSource.autoSync` | **wird umbenannt zu `ragIndexEnabled`** (oder bleibt + Doku — Entscheidung in Phase A) — neuer Sinn: „dieses Tree-Element wird in den RAG indexiert" |
| `WorkspaceRagInsightsPage` (Workspace-scoped) | **gelöscht**, ersetzt durch globale `Start > Nutzung > RAG-Inventar` |
| `/api/workspace/{instanceId}/rag-statistics` | **gelöscht**, ersetzt durch `/api/rag/inventory/{me\|mandate\|platform}` |
| `BackgroundJob` ohne Cancel-API | **erweitert**: `cancelJob(jobId)`, Walker-Cancel-Check |
### Wizard-Vereinfachung — alle Connector-Typen über denselben Pfad
Der `AddConnectionWizard` ist **die einzige** Eintragungs-UI für neue Connections. Alle heute parallelen Buttons auf der `ConnectionsPage` (separater **„Infomaniak"**-Button mit eigener Modal-Logik, separater **„Admin-Zustimmung"**-Button für Microsoft) **werden in den Wizard integriert**. Auf der `ConnectionsPage` bleibt nur **„Verbindung hinzufügen"** als Eintrittspunkt.
**Connector-Type-Aware Wizard:** Der Wizard hat eine Basis-Sequenz und **typ-spezifische Zusatz-Schritte**, die nach „Anbieter wählen" eingehängt werden.
#### Basis-Sequenz (alle Typen)
1. **Anbieter wählen** — Cards für Google / Microsoft 365 / ClickUp / Infomaniak (Infomaniak kommt also wieder in die Auswahl).
2. *(typ-spezifischer Zusatz-Schritt — falls vorhanden, siehe unten)*
3. **Rechtliche Einwilligung** (Knowledge Ingestion ja/nein) — kurzer Hinweistext: „Du wählst nach dem Verbinden in der Datenleiste konkret, welche Ordner/Postfächer/Listen indexiert werden sollen."
4. **Verbindungsschritt** — typ-abhängig: OAuth-Popup (google/msft/clickup) **oder** PAT-Eingabe (infomaniak).
#### Connector-Type-spezifische Zusatz-Schritte
| Connector | Zusatz-Schritt nach „Anbieter wählen" | Inhalt |
|-----------|--------------------------------------|--------|
| **Microsoft 365** | **Optional**: Admin-Zustimmung (Tenant-weit) | Hinweis: „Falls dein Tenant einen Admin-Consent erfordert, kann er **vor** der persönlichen OAuth einmalig erteilt werden. Sonst überspringe diesen Schritt." Button öffnet `/api/msft/adminconsent` Popup. Nach Schliessen → weiter zu Step 3. |
| **Google** | | (Standard OAuth) |
| **ClickUp** | | (Standard OAuth) |
| **Infomaniak** | **Pflicht**: Personal Access Token | Inline-Eingabefeld + Erklärung wie heute im `infomaniakModal`. Erst nach valider Token-Validierung → weiter zu Step 3. Connection wird beim PAT-Submit erstellt (vergleichbar mit `createInfomaniakConnection` + `submitInfomaniakToken`); kein OAuth-Popup. |
**Mechanik:** Der Wizard hält pro Connector-Typ eine kleine Step-Definition (z.B. `WIZARD_STEPS_BY_TYPE: Record<ConnectorType, WizardStep[]>`), die der Reihe nach durchlaufen wird. Stepper im UI passt sich dynamisch an (z.B. 4 Punkte für Microsoft mit Admin-Zustimmung, 3 für Google).
**Entfernt von `ConnectionsPage`:**
- ~~Button „Admin-Zustimmung"~~ — wandert in den Wizard (Microsoft-Pfad).
- ~~Button „Infomaniak"~~ — wandert in den Wizard (eigener Connector-Typ in der Anbieter-Auswahl).
- ~~`infomaniakModal`-State~~ — wandert in den Wizard.
- ~~`handleAdminConsent`~~ — wandert in den Wizard.
**Entfernt aus dem heutigen Wizard:**
- ~~Step 2 (Preferences mit Neutralisierung-Checkbox, Mail-Tiefe etc.)~~ — wandert in Connection-Detail-Drawer bzw. in die UDB (Neutralize per Element).
- ~~**Kostenabschätzung**~~ — wird komplett entfernt. Begründung: Schätzung war unzuverlässig (Token-Annahmen pro Mail spekulativ; Drive/SharePoint-Datenmenge nicht bekannt vor dem ersten Walk), erzeugt falsche Sicherheit, und stoppt User unnötig vom Connect. Kostenkontrolle erfolgt **nachgelagert**: über die Stop-Funktion und über das Per-Element-Opt-In.
**Vorteil:** Genau ein Eintrittspunkt für jeden Connector-Typ. Konsistente UX. Die echte Konfiguration (was indexieren, was neutralisieren) passiert dort, wo der User die Daten sieht — in der UDB.
### Wo sieht und stoppt der User die Indexierung? (Sichtbarkeit-Spec)
Diese Frage muss **drei** UI-Plätze ohne Lücke abdecken:
| Ort | Was sichtbar | Aktionen |
|-----|--------------|----------|
| **Globales Header-Indikator** (klein, oben rechts neben User-Menü) | Badge mit Anzahl gerade laufender Bootstrap-/Resync-Jobs des aktuellen Users (z.B. „🔄 2"). Klick → öffnet RAG-Inventar-Seite. Verschwindet, wenn keine Jobs laufen. Polling: alle 510s. | Click-through zur RAG-Inventar |
| **`ConnectionsPage`, pro Row** | Pro Connection mit aktivem Bootstrap-Job: Status-Pill „Indexierung läuft" + Progress (`processed / known total` oder Prozent) + Roter **Stop**-Button. Bei finished: „Letzter Sync: <Datum>, X indexiert / Y skipped / Z failed". | **Stop**-Button → `POST /api/connections/{id}/knowledge-stop`. **Master-Toggle** (off → Confirm → Purge + Cancel). |
| **`RagInventoryPage` Tab „Meine Daten"** | Vollständige Sicht: pro Connection eine Card; pro Card alle indexierten DataSources mit Item-Count + letztem Sync; Status-Pill bei laufendem Bootstrap; pro DataSource der UDB-Index-Toggle nochmals als Quick-Off. | **Stop**-Button pro Connection (gleiche API). **Toggle off** pro DataSource → Mini-Purge. **Master-Toggle** der Connection. **„Jetzt synchronisieren"**-Button → enqueue Bootstrap on-demand. |
**Backend-Voraussetzung für die Sichtbarkeit:** `GET /api/rag/connection/{id}/status` liefert für eine Connection `{ runningJobId?: string, runningJobProgress?: number, lastRun?: { finishedAt, indexedNew, skippedDuplicate, failed }, nextScheduledResync?: timestamp }`. `GET /api/rag/inventory/me` aggregiert das für alle Connections und liefert zusätzlich `runningJobsTotal` für das Header-Badge.
### Kritische Stellen im Code
- `gateway/modules/serviceCenter/services/serviceKnowledge/subConnectorIngestConsumer.py` — Bootstrap-Dispatcher: muss Walker auf DataSource-Iteration umstellen + Cancel-Flag prüfen.
- `gateway/modules/serviceCenter/services/serviceKnowledge/subConnectorSync*.py` (5 Walker) — konsumieren `DataSource`-Liste statt globalem Authority-Walk; lesen `neutralize` aus DataSource; prüfen periodisch `cancelRequested`.
- `gateway/modules/serviceCenter/services/serviceKnowledge/subConnectorPrefs.py``neutralizeBeforeEmbed`, `mimeAllowlist`, `surfaceToggles` raus.
- `gateway/modules/datamodels/datamodelUam.py``knowledgePreferences` Schema-Doku anpassen.
- `gateway/modules/datamodels/datamodelDataSource.py``autoSync``ragIndexEnabled` (oder umdokumentieren); ggf. `lastIndexed`-Feld hinzufügen.
- `gateway/modules/serviceCenter/services/serviceBackgroundJobs/mainBackgroundJobService.py``cancelJob(jobId)` API hinzufügen; `JobProgressCallback` um `isCancelled()` erweitern.
- `gateway/modules/routes/routeDataConnections.py``GET` liefert Knowledge-Felder; neue PATCH-Endpoints für Consent + Preferences + Stop.
- `gateway/modules/routes/routeDataSources.py` — neuer PATCH `/{id}/rag-index` mit Purge bei Off-Toggle.
- `gateway/modules/interfaces/interfaceDbKnowledge.py``deleteFileContentIndexByConnectionAndPathPrefix(connectionId, pathPrefix)` und `listFileContentIndexByConnection(connectionId)` und `listFileContentIndexByDataSource(dataSourceId)`.
- `gateway/modules/features/workspace/routeFeatureWorkspace.py``rag-statistics` Methode löschen.
- **NEU** `gateway/modules/routes/routeRagInventory.py``/api/rag/inventory/{me,mandate,platform}` und `/api/rag/connection/{id}/status`.
- `gateway/modules/system/mainSystem.py` — Navigation `Nutzung > RAG-Inventar`.
- `frontend_nyla/src/pages/views/workspace/WorkspaceRagInsightsPage.tsx` + `.module.css`**löschen**.
- `frontend_nyla/src/types/mandate.ts``rag-insights` raus.
- `frontend_nyla/src/pages/FeatureView.tsx`, `App.tsx` — Route + Mapping raus.
- `frontend_nyla/src/components/AddConnectionWizard/AddConnectionWizard.tsx` — Connector-Type-Aware Step-Definition; integrierte Microsoft-Admin-Consent + Infomaniak-PAT Steps; Kostenabschätzung + Preferences-Step entfernen.
- `frontend_nyla/src/components/UnifiedDataBar/SourcesTab.tsx` — vierter Action-Button für `ragIndexEnabled` (analog zu 🔒/Neutralize) **inklusive Vererbungs-Visualisierung** (`inheritedRagIndexEnabled`-Prop, gestrichelt/gedimmt für vererbte Werte).
- `frontend_nyla/src/api/connectionApi.ts``KnowledgePreferences` reduzieren; neue API-Methoden.
- **NEU** `frontend_nyla/src/pages/system/RagInventoryPage.tsx` — Drei-Tab-Seite (Meine / Mandant / Plattform).
- `frontend_nyla/src/pages/basedata/ConnectionsPage.tsx` — Buttons „Admin-Zustimmung" + „Infomaniak" + Modal **entfernen**; Master-Toggle pro Row + Status-Pill + Stop-Button bei laufendem Bootstrap hinzufügen.
- **NEU** `frontend_nyla/src/components/Header/RagRunningBadge.tsx` — globales Header-Badge mit Anzahl laufender Bootstrap-Jobs + Click-through zur RAG-Inventar.
### Bekannte Fallstricke
- **Migration `neutralizeBeforeEmbed=true` → Pro-DataSource:** Bei Schema-Update werden alle existierenden DataSources der Connection mit `neutralize=true` markiert. Falls keine DataSources existieren (was wahrscheinlich ist, weil heute Walker OAuth-global läuft), gehen die Werte verloren — Release-Note nötig.
- **Migration `autoSync`-Bedeutung:** Wenn `autoSync` heute schon mit anderem Sinn benutzt wird, ist Renaming sicherer als Umdeutung. Phase A startet mit Code-Audit.
- **Bestehende RAG-Daten ohne DataSource-Bezug:** Heute haben SharePoint-/Outlook-Chunks `connectionId` aber keinen DataSource-Link (Walker walked OAuth-global). Migration: bestehende Chunks bleiben drin, neue Indexierung läuft nur über DataSources. „Legacy-Chunks" sichtbar machen oder einmalig purgen — **offene Entscheidung**.
- **Cancel-Race-Condition:** Walker prüft Cancel-Flag alle N Items. Im Worst Case dauert Stop noch N Items lang. Akzeptabel für v1.
- **Daily Resync vs. Cancel:** Wenn ein Daily-Resync läuft und User stoppt — startet er morgen neu? Ja, das ist gewollt. Wenn User dauerhaft Pause will → Master-Toggle off oder einzelne DataSource-Toggles off.
## Ziel und Nicht-Ziele
**Ziel:**
- **Single Source of Truth** für Neutralisierung: ausschliesslich `DataSource.neutralize`, gemanagt in der UDB.
- **Per-Element RAG-Index-Toggle** in der UDB (analog zu Scope/Neutralize).
- **Vollständiger Lebenszyklus** für Wizard-Consent (Erteilen, Widerrufen) im UI sichtbar.
- **Stop-Funktion** für laufende Bootstrap-Jobs (Job-Cancel + Walker-Cancel-Check).
- **Globale `Start > Nutzung > RAG-Inventar`** Seite mit drei Tabs (Meine Daten / Mandant / Plattform).
- **Bootstrap-Walker konsumieren `DataSource`-Liste** (nur jene mit `ragIndexEnabled = true`).
- **`WorkspaceRagInsightsPage` ist gelöscht.**
- **Wizard ist auf Anbieter + Consent reduziert** (keine Kostenschätzung, keine Preferences, keine Neutralisierung).
- **Tote Pref-Felder entfernt** (`mimeAllowlist`, `surfaceToggles`, `neutralizeBeforeEmbed`).
**Explizit NICHT:**
- Wir bauen **keinen** zweiten Knowledge-Store.
- Wir verändern **nicht** die Retrieval-Seite (`buildAgentContext`).
- Wir bauen **kein** Per-Item-Revoke (Granularität: Element-Toggle in UDB oder Master-Connection-Toggle — nicht eine einzelne Mail).
- Wir bauen **keine Auto-Default-DataSources** beim Connect — User muss aktiv Indexierung pro Element wählen.
- Wir bauen **keine Echtzeit-Cost-Caps** (z.B. „stop bei CHF 5.-") — v1: Stop ist manuell. Cost-Caps sind Roadmap v2.
## Betroffene Module
- **Gateway:** `serviceKnowledge` (Walker-Refactor, Purge-Endpoints, Cancel-Check), `serviceBackgroundJobs` (Cancel-API), `routeDataConnections` (Knowledge-Felder + PATCH-Endpoints), `routeDataSources` (PATCH `/rag-index`), neuer `routeRagInventory`, `mainSystem` (Navigation), `datamodelUam`, `datamodelDataSource`.
- **Frontend:** `WorkspaceRagInsightsPage` löschen, `AddConnectionWizard` stark vereinfachen (Steps, keine Kostenschätzung), `ConnectionsPage` (Master-Toggle + Stop-Button), `SourcesTab` (4. Index-Toggle-Button), neue `RagInventoryPage`, `pageRegistry`.
- **DB-Migration:** ja — `knowledgePreferences.neutralizeBeforeEmbed/mimeAllowlist/surfaceToggles` raus; `DataSource.autoSync``ragIndexEnabled` (oder Re-Doku).
- **Andere Komponenten:** keine.
## Entscheidungen
| Datum | Entscheidung | Begründung |
|-------|-------------|------------|
| 2026-05-12 | Neutralisierung wird ausschliesslich pro `DataSource` verwaltet | Single Source of Truth; UDB ist der einzige Ort für Datenobjekt-Policies |
| 2026-05-12 | Index-Aktivierung wird pro Tree-Element via UDB-Toggle gesteuert (analog Scope/Neutralize) | Konsistente UX, granular, revoke-trivial |
| 2026-05-12 | `WorkspaceRagInsightsPage` wird gelöscht, ersetzt durch globale `Start > Nutzung > RAG-Inventar` | RAG ist plattformweit, nicht workspace-scoped |
| 2026-05-12 | Walker iterieren über `DataSource`-Rows mit `ragIndexEnabled=true` der Connection | User-kontrollierter Scope; per-Source Revoke trivial |
| 2026-05-12 | **Keine Auto-Default-DataSources** beim OAuth-Connect | DSGVO-konformer (kein Default-Indexing); User muss aktiv wählen |
| 2026-05-12 | **Kostenabschätzung im Wizard wird entfernt** | Unzuverlässig, erzeugt falsche Sicherheit; Kostenkontrolle via Stop-Button + Per-Element-Opt-In |
| 2026-05-12 | **Stop-Mechanismus**: implizit (Toggle off) + explizit (Cancel-Job-Button) | Jederzeit stoppbar, kein „Walker rennt weg" |
| 2026-05-12 | „Was ist neu zu indexieren" wird **nicht** neu erfunden — bestehende Hash-Idempotenz reicht | Pure Function über Content; bereits implementiert und getestet |
| 2026-05-12 | Surface Toggles und MIME-Allowlist werden aus Schema **entfernt** (nicht implementiert) | Per-Element-Toggle macht sie obsolet |
| 2026-05-12 | **Vererbungs-Mechanik für Tree-Policies**: `neutralize` und `ragIndexEnabled` kaskadieren auf Container, lokale Overrides möglich (analog zu `inheritedScope`/`inheritedNeutralize` Pattern) | Konsistenz mit existierender UDB-Logik; spart redundante Per-File-Toggles |
| 2026-05-12 | **Tabellen-Granularität**: Neutralisierung pro Feld (`neutralizeFields[]`); RAG-Index pro Tabelle (kein Per-Feld) | Bestehende UI-Pattern bleiben; Per-Feld-Index wäre Over-Engineering für v1 |
| 2026-05-12 | **Wizard ist einziger Eintrittspunkt für alle Connector-Typen** — Buttons „Admin-Zustimmung" + „Infomaniak" auf `ConnectionsPage` werden entfernt und in den Wizard integriert | Genau ein Pfad pro Connector-Typ; eliminiert Parallelmechanismen |
| 2026-05-12 | **Drei Sichtbarkeits-Orte für laufende Indexierung**: Header-Badge, ConnectionsPage-Row, RagInventoryPage-Tab | User darf nirgends „raten" was läuft; Stop-Aktion an mindestens zwei Orten erreichbar |
## Umsetzungs-Checkliste
### Phase A — Datenmodell & Cancel-Infrastruktur (Tage 13)
- [x] **Audit:** existierende Verwendung von `DataSource.autoSync` prüfen → Entscheidung: Renaming `ragIndexEnabled` vs. Re-Doku.
- [x] `datamodelDataSource.DataSource`: `ragIndexEnabled: bool = False` (default `False` — kein Auto-Index ohne explizite Wahl). `lastIndexed: Optional[float]`.
- [x] Migration: `script_db_migrate_datasource_rag.py` (Rename `autoSync``ragIndexEnabled`, `lastSynced``lastIndexed`).
- [x] `datamodelUam.UserConnection.knowledgePreferences` — Schema-Doku: `neutralizeBeforeEmbed`, `surfaceToggles`, `mimeAllowlist` raus.
- [x] `subConnectorPrefs.ConnectionIngestionPrefs` — tote Felder entfernt (nur Mail/ClickUp/File-Prefs bleiben).
- [x] `serviceBackgroundJobs.cancelJob(jobId) -> bool` API hinzugefügt (setzt Status auf `CANCELLED`).
- [x] `JobProgressCallback`: `isCancelled() -> bool` (liest Job-Status aus DB; cached 3s).
- [x] **Walker-Refactor:** Alle 5 `subConnectorSync*.py` Walker iterieren über `ragIndexEnabled=true` DataSources, Cancel-Check alle 50 Items, `provenance.dataSourceId`.
- [x] **Purge-Helpers** in `interfaceDbKnowledge`: `deleteFileContentIndexByDataSource(dataSourceId)`.
### Phase B — APIs (Tage 35)
- [x] `GET /api/connections/` Antwort um `knowledgeIngestionEnabled` und `knowledgePreferences` erweitert.
- [x] `PATCH /api/connections/{id}/knowledge-consent` mit Purge + Cancel bei `false`, Bootstrap bei `true`.
- [x] `PATCH /api/connections/{id}/knowledge-preferences` — speichert Mail-Tiefe etc.
- [x] `POST /api/connections/{id}/knowledge-stop` — cancelled alle laufenden Bootstrap-Jobs der Connection.
- [x] **`PATCH /api/datasources/{id}/rag-index`** mit Mini-Bootstrap bei `true`, Purge bei `false`.
- [x] **Neue Route `routeRagInventory`:** `/me`, `/mandate`, `/platform`, `/jobs`, `/reindex/{connectionId}`.
- [ ] ~~`GET /api/rag/connection/{id}/status`~~**Entscheidung: nicht umgesetzt** — Status-Infos laufen über Inventory-Aggregation.
- [x] **Workspace-RAG-Endpoint gelöscht:** `routeFeatureWorkspace.rag-statistics` raus.
### Phase C — Frontend Cleanup (Tage 56)
- [x] **Gelöscht:** `WorkspaceRagInsightsPage.tsx` + `.module.css`.
- [x] `mandate.ts`: `rag-insights` Workspace-View raus (2026-05-15).
- [x] `FeatureView.tsx`: Guard-Condition für `rag-insights` entfernt (2026-05-15).
- [x] `App.tsx`: Route `rag-insights` entfernt (2026-05-15).
- [x] **Wizard-Refactor (`AddConnectionWizard`):** Connector-Type-Aware Steps (Google/MSFT/ClickUp/Infomaniak), MSFT Admin-Consent + Infomaniak-PAT integriert, Cost-Logik + Prefs-Step entfernt.
- [x] **`ConnectionsPage` Cleanup:** Admin-Zustimmung + Infomaniak Standalone-Buttons entfernt (nur noch via Wizard).
### Phase D — Frontend UDB & RAG-Sichtbarkeit (Tage 69)
- [x] **`SourcesTab.tsx`:** 4. Action-Button (🧠) pro Tree-Node mit Vererbungs-Visualisierung (`inheritedRagIndex`).
- [x] **`RagInventoryPage`** ist der **primäre** Ort für Knowledge-Consent-Toggle, Stop-Button und Sync-Status pro Connection. **Entscheidung:** `ConnectionsPage` erhält **keine** duplizierte Master-Toggle/Stop-UI — Nutzer navigieren für RAG-Management zur RagInventoryPage.
- [ ] ~~`ConnectionsPage.tsx` Master-Toggle + Status-Pill + Stop-Button pro Row~~**bewusst nicht umgesetzt** (Preferences und RAG-Steuerung primär auf RagInventoryPage).
- [ ] ~~`KnowledgePreferencesDrawer`~~**bewusst nicht umgesetzt** (Preferences über RagInventoryPage oder Backend-Defaults).
- [x] **Globales Header-Indikator:** `RagRunningBadge` als Floating-Komponente in `MainLayout.tsx`, Polling gegen `/api/rag/inventory/jobs`.
### Phase E — Frontend RAG-Inventar-Seite (Tage 811)
- [x] **Neue Seite `RagInventoryPage`** mit Consent-Toggle, Stop-Button, Reindex-Button, DataSource-Übersicht pro Connection.
- [x] `pageRegistry.tsx` + `App.tsx`: Route eingerichtet.
- [x] `mainSystem.py` Navigation: Eintrag `Nutzung > RAG-Inventar`.
### Phase F — DSGVO Sichtbarkeit + Tests (Tage 1113)
- [x] **DSGVO-Audit-Log:** `knowledge_consent_changed`, `rag_index_toggled`, `knowledge_jobs_stopped` werden via `audit_logger` geloggt.
- [x] **Backend-Integrationstests:** manuell verifiziert (Smoke-Tests, 2026-05-15).
- [x] **Frontend-Tests:** manuell verifiziert (2026-05-15).
- [x] **Wiki-Updates:** `b-reference/gateway/ai-agent.md` aktualisiert; `TOPICS.md` enthält RAG-Inventar-Eintrag.
## Akzeptanzkriterien
| # | Kriterium (Given-When-Then) | Prio |
|---|---------------------------|------|
| 1 | **Given** ich habe Wizard-Consent `true`, **when** ich auf der `ConnectionsPage` den Master-Toggle auf `off` setze und bestätige, **then** werden alle `FileContentIndex` + `ContentChunk`-Rows der Connection synchron gelöscht, `knowledgeIngestionEnabled = false` gesetzt, und alle laufenden Bootstrap-Jobs dieser Connection cancelled. | must |
| 2 | **Given** in der UDB ist für DataSource X `ragIndexEnabled = true` und `neutralize = true`, **when** der Bootstrap/Resync läuft, **then** werden Items aus diesem Pfad **mit** Neutralisierung indexiert; **and** Items aus anderen DataSources der gleichen Connection werden nur indexiert wenn ihr eigener `ragIndexEnabled = true` ist. | must |
| 3 | **Given** in der UDB klicke ich den Index-Toggle einer DataSource auf `off` und bestätige, **then** wird `ragIndexEnabled = false` gesetzt, ein Purge-Job entfernt alle `FileContentIndex`-Rows mit matching `connectionId` + `pathPrefix`, und Toast zeigt „X Inhalte aus RAG entfernt". | must |
| 4 | **Given** ein Bootstrap-Job läuft (`status=RUNNING`), **when** ich den Stop-Button drücke, **then** wird `cancelJob` aufgerufen, der Walker beendet sich nach max. ~50 Items, Job-Status wird `CANCELLED`, Result enthält `cancelled: true, processedSoFar: N`, bereits indexierte Items bleiben drin. | must |
| 5 | **Given** ich öffne `Start > Nutzung > RAG-Inventar`, **then** sehe ich Tab „Meine Daten" mit allen meinen Connections + DataSources + Item-Counts + letztem Sync + Stop-Button bei laufenden Jobs. **And** Tab „Mandant" / „Plattform" nur, wenn ich entsprechende Rolle habe. | must |
| 6 | **Given** der `WorkspaceRagInsightsPage`-Code, **then** existiert er nicht mehr im Repo; `rag-insights` ist aus `mandate.ts`, `FeatureView.tsx`, `App.tsx` entfernt; `/api/workspace/{instanceId}/rag-statistics` liefert 404. | must |
| 7 | **Given** der `AddConnectionWizard`, **then** zeigt er **keine** Kostenabschätzung, **keine** Neutralisierungs-Checkbox und **keinen** Preferences-Step. Nur: Anbieter → Consent → Connect. | must |
| 8 | **Given** `UserConnection.knowledgePreferences`, **then** existieren die Felder `neutralizeBeforeEmbed`, `surfaceToggles`, `mimeAllowlist` nicht mehr im Schema und sind in keinem Walker referenziert. Migration: bestehende `neutralizeBeforeEmbed=true`-Werte wurden auf zugehörige DataSources übertragen. | must |
| 9 | **Given** ich frisch Microsoft verbinde mit Wizard-Consent `true` und ich habe noch keine DataSources mit `ragIndexEnabled=true` angelegt, **then** läuft **kein** Bootstrap (Walker findet leere DataSource-Liste, Job endet sofort mit `processed: 0`). | must |
| 10 | **Given** in der UDB sind 3 Tree-Elemente mit `ragIndexEnabled=true` markiert, **when** ich auf einer Connection-Row in `ConnectionsPage` „Knowledge sync starten" drücke (oder der Daily-Resync läuft), **then** indexiert der Walker exakt diese 3 Pfade, und der dedup-Mechanismus skippt unveränderte Items per Hash. | must |
| 11 | **Given** jeder Toggle (Connection-Master, DataSource-Index, DataSource-Neutralize), **when** er geändert wird, **then** wird der Wechsel im `aiAuditLogger` (oder Audit-Log) festgehalten mit `userId, timestamp, target, oldValue, newValue`. | should |
| 12 | **Given** ich öffne den Knowledge-Preferences-Edit-Drawer auf einer Connection-Row, **then** sehe und editiere ich `mailContentDepth`, `mailIndexAttachments`, `clickupScope`, `maxAgeDays` (aber **nicht** `neutralizeBeforeEmbed` — dieses Feld existiert nicht mehr). | should |
| 13 | **Given** ich setze in der UDB `neutralize=true` auf einem **SharePoint-Site-Knoten** (Container) und `ragIndexEnabled=true` auf demselben, **when** der Walker den Site indexiert, **then** werden **alle** Files unter dem Site mit Neutralisierung indexiert; **and** wenn ein Subfolder eine eigene `neutralize=false` Policy hat, werden dessen Files **ohne** Neutralisierung indexiert (Vererbung mit lokaler Override). | must |
| 14 | **Given** ein `FeatureDataSource` für eine Trustee-Tabelle mit `neutralizeFields=["email","name"]` und `ragIndexEnabled=true`, **when** der Walker indexiert, **then** sind in den persistierten Chunks die Felder `email` und `name` neutralisiert, alle anderen Felder im Klartext. | must |
| 15 | **Given** auf der `ConnectionsPage` existieren keine separaten Buttons mehr für „Admin-Zustimmung" und „Infomaniak", **when** ich „Verbindung hinzufügen" klicke und „Microsoft" wähle, **then** zeigt der Wizard einen optionalen „Admin-Zustimmung"-Step mit Hinweistext und Skip-Möglichkeit; **and** wenn ich „Infomaniak" wähle, zeigt er einen PAT-Eingabe-Step. | must |
| 16 | **Given** ich starte im Wizard einen Infomaniak-Connect, **when** ich im PAT-Step abbreche, **then** wird die im Backend angelegte pending Connection wieder gelöscht (kein Orphan), analog zur heutigen `handleInfomaniakCancel`-Logik. | must |
| 17 | **Given** ein Bootstrap-Job einer Connection läuft, **then** sehe ich an **drei** Orten den Status: (a) globales Header-Badge mit Anzahl laufender Jobs, (b) `ConnectionsPage`-Row mit Status-Pill + Stop-Button, (c) `RagInventoryPage` Tab „Meine Daten" mit Detail + Stop-Button. Alle drei Orte zeigen dieselben Counter (gleicher Backend-Endpoint). | must |
| 18 | **Given** ich klicke das globale Header-Badge, **then** werde ich zur `RagInventoryPage` (Tab „Meine Daten") navigiert. | should |
## Testplan
| ID | AC | Art | Automatisiert | Repo-Pfad | Status |
|----|----|-----|--------------|-----------|--------|
| T1 | 1 | api+integration | manuell | Smoke-Test | manuell verifiziert (2026-05-15) |
| T2 | 2 | unit | manuell | Smoke-Test | manuell verifiziert (2026-05-15) |
| T3 | 3 | api+integration | manuell | Smoke-Test | manuell verifiziert (2026-05-15) |
| T4 | 4 | unit+integration | manuell | Smoke-Test | manuell verifiziert (2026-05-15) |
| T5 | 5 | api+ui | manuell | Smoke-Test | manuell verifiziert (2026-05-15) |
| T6 | 6 | repo+api | manuell | Smoke-Test | manuell verifiziert (2026-05-15) |
| T7 | 7 | ui | manuell | Smoke-Test | manuell verifiziert (2026-05-15) |
| T8 | 8 | unit+migration | manuell | Smoke-Test | manuell verifiziert (2026-05-15) |
| T9 | 9 | integration | manuell | Smoke-Test | manuell verifiziert (2026-05-15) |
| T10 | 10 | integration | manuell | Smoke-Test | manuell verifiziert (2026-05-15) |
| T11 | 11 | unit | manuell | Smoke-Test | manuell verifiziert (2026-05-15) |
| T12 | 12 | ui | manuell | Smoke-Test (ConnectionsPage RAG-UI bewusst nicht umgesetzt, D-6) | manuell verifiziert (2026-05-15) |
| T13 | 13 | integration | manuell | Smoke-Test | manuell verifiziert (2026-05-15) |
| T14 | 14 | integration | manuell | Smoke-Test | manuell verifiziert (2026-05-15) |
| T15 | 15 | ui | manuell | Smoke-Test | manuell verifiziert (2026-05-15) |
| T16 | 16 | ui+integration | manuell | Smoke-Test | manuell verifiziert (2026-05-15) |
| T17 | 17 | ui+e2e | manuell | Smoke-Test | manuell verifiziert (2026-05-15) |
| T18 | 18 | ui | manuell | Smoke-Test | manuell verifiziert (2026-05-15) |
## Links
- Vorgänger-Plan: `wiki/c-work/4-done/2026-04-id-unified-knowledge-indexing-rag-concept.md`
- Architektonisch betroffen: `wiki/b-reference/platform/neutralization.md`, `wiki/b-reference/gateway/ai-agent.md`, `wiki/b-reference/frontend-nyla/architecture.md`
- Code-Touchpoints: siehe Abschnitt „Kritische Stellen im Code"
## Abschluss
- [x] `b-reference/gateway/ai-agent.md` aktualisiert (Knowledge Lifecycle, DataSource-getriebener Walker, Cancel-Mechanismus)
- [x] `b-reference/platform/neutralization.md` aktualisiert (DataSource = SSoT, kein neutralizeBeforeEmbed mehr, Tree-Vererbung via subPolicyResolver) — 2026-05-15
- [x] `b-reference/frontend-nyla/architecture.md` aktualisiert (RagInventoryPage, UDB 4. Action-Button, RagRunningBadge, AddConnectionWizard) — 2026-05-15
- [x] `TOPICS.md` aktualisiert (RAG-Inventar als neuer Kanon-Eintrag)
- [x] Dieses Dokument in `c-work/4-done/` (Status → `done`, 2026-05-15)
- [x] Eintrag im `c-work/_CHANGELOG.md`