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

746 lines
41 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.

---
title: "RAG Consent & Control Implementierungsplan"
status: 4-done
completed: 2026-05-15
owner: ida
relatedConcept: ./2026-05-rag-consent-and-control-unification.md
created: 2026-05-12
lastReviewed: 2026-05-15
---
# RAG Consent & Control — Implementierungsplan
> **Lese-Kontext:** Architektonisches Konzept und Begründungen siehe Schwesterdokument
> [`2026-05-rag-consent-and-control-unification.md`](./2026-05-rag-consent-and-control-unification.md).
> Dieses Dokument ist die **fein granulare, code-zentrierte Umsetzung** mit Phasen, Datei-Änderungen,
> Acceptance-Gates und Test-Hooks. Reihenfolge ist verbindlich (Dependency-Order).
---
## 0. Audit-Befunde (Stand 2026-05-12)
Die folgenden Befunde sind das Ergebnis des Code-Audits und sind die **Grundannahmen** dieses Plans.
Jede Phase referenziert sie.
| # | Befund | Datei / Stelle | Konsequenz für Plan |
|---|--------|----------------|---------------------|
| F1 | `DataSource.autoSync` ist nur im Model deklariert, **wird im Code nirgends gelesen oder geschrieben** (Suche in `gateway`, `frontend_nyla` ergab Null Hits ausserhalb der Modeldatei). | `datamodels/datamodelDataSource.py:6569` | Renaming auf `ragIndexEnabled` ist **risikofrei**; Migration = `ALTER TABLE … RENAME COLUMN`. |
| F2 | `BackgroundJobStatusEnum.CANCELLED` und `TERMINAL_JOB_STATUSES = {SUCCESS, ERROR, CANCELLED}` existieren bereits, aber `cancelJob()`-API fehlt; `JobProgressCallback` hat nur `(progress, message)`-Signatur. | `serviceBackgroundJobs/mainBackgroundJobService.py:52, 157`, `datamodels/datamodelBackgroundJob.py:34, 3741` | Cancel-Infrastruktur muss ergänzt werden, **nicht** das Status-Enum. |
| F3 | Bootstrap-Dispatcher ruft Walker mit `connectionId` auf; Walker enumerieren OAuth-global (`adapter.browse("/")`) und laden Prefs intern. | `subConnectorIngestConsumer.py:168221`, `subConnectorSyncSharepoint.py:110174` | Walker-API muss um optionalen `dataSources: List[DataSource]`-Parameter erweitert werden; Default-Verhalten bleibt rückwärtskompatibel **bis Phase D** umstellt. |
| F4 | `GET /api/connections/` liefert `knowledgeIngestionEnabled` / `knowledgePreferences` **nicht** im Response. | `routes/routeDataConnections.py:188201, 249264, 282298` (3 Stellen) | Response-Builder zentralisieren als Helper, dann `_buildEnhancedItems` + `groupSummary` + Haupt-Block daraus speisen. |
| F5 | `routeDataSources.py` hat sauberes PATCH-Pattern für `/scope`, `/neutralize`, `/neutralize-fields`. | `routes/routeDataSources.py:43127` | Neues PATCH `/rag-index` als 4. Endpoint mit identischer Struktur. |
| F6 | `audit_logger.logEvent(...)` mit `AuditCategory.PERMISSION` existiert — keine neue Audit-Infrastruktur nötig. | `shared/auditLogger.py:96`, `datamodels/datamodelAudit.py:2634` | Consent- und Toggle-Änderungen via existierendem Logger → eine Codezeile pro PATCH. |
| F7 | `WorkspaceRagInsightsPage` ist via UI-Permission `ui.feature.workspace.rag-insights` an Workspace-Rollen gebunden (3 Vorkommen in `mainWorkspace.py`). | `features/workspace/mainWorkspace.py:3738, 89, 100`; `features/workspace/routeFeatureWorkspace.py:2219` | Löschung muss UI-Permission **und** Backend-Route entfernen — sonst tote Permission im RBAC-Tree. |
| F8 | `AddConnectionWizard` hat 4-Schritt-Flow: Connector → Consent → Prefs → Summary mit `computeCostEstimate` (Z. 71151), `neutralizeBeforeEmbed`-Checkbox (Z. 314320), Cost-Tabelle (Z. 458493). MSFT Admin-Consent + Infomaniak laufen über separate Buttons in `ConnectionsPage.tsx` (Z. 245263, `adminConsentPending` State Z. 57). | `components/AddConnectionWizard/AddConnectionWizard.tsx`, `pages/basedata/ConnectionsPage.tsx` | Wizard wird komplett umgebaut: 3-Schritt-Basis-Flow + connector-spezifische Zwischen-Steps; Cost-Logik + Prefs-Step + Neutralize-Checkbox raus. |
| F9 | `SourcesTab.tsx` hat existierendes `inheritedScope`/`inheritedNeutralize`-Pattern in Tree-Renderern (Z. 11121592). Toggle-Implementierungen via `_toggleNeutralizeField` (Z. 715), `cycleScope`-Handler bei Zeile 1694. | `components/UnifiedDataBar/SourcesTab.tsx` | `inheritedRagIndexEnabled` folgt **exakt** demselben Pattern; kein neuer Mechanismus, nur Erweiterung der bestehenden Props + Render-Pfade. |
| F10 | `IngestionJob` hat bereits `neutralize: bool`-Feld (siehe SharePoint-Walker Z. 359). | `subConnectorSyncSharepoint.py:349362` | Kein Schema-Wechsel an `IngestionJob` nötig — Walker liefert `neutralize` aus DataSource statt aus Pref. |
---
## 1. Phasen-Übersicht (Dependency-Order)
```
Phase A: Datenmodell + Cancel-API (Backend, keine UI-Wirkung)
├─► Phase B: Walker-Refactor (Backend; nutzt A)
│ │
│ └─► Phase E: Tests E2E (parallel ab Phase D-Mitte möglich)
├─► Phase C: Connection-Routes (Backend; nutzt A; benötigt für Phase D)
│ │
│ └─► Phase D-Frontend (siehe unten)
└─► Phase D: Frontend (UDB-Toggle + Wizard + ConnectionsPage + RagInventoryPage)
└─► Phase F: WorkspaceRagInsights-Löschung + Doku
```
Reihenfolge ist verbindlich. Phase D darf **nicht** vor Phase A+C abgeschlossen sein, da Frontend-API-Calls
sonst ins Leere laufen. Phase B kann parallel zu Phase C laufen, sobald A fertig ist.
---
## 2. Phase A — Datenmodell & Cancel-Infrastruktur
**Ziel:** Schema-Änderungen + Cancel-API verfügbar, ohne dass irgendeine bestehende Funktion bricht.
### A.1 `DataSource`-Modell umbenennen
- **Datei:** `gateway/modules/datamodels/datamodelDataSource.py`
- **Änderung:**
- `autoSync: bool = Field(default=False, ...)`**umbenennen** zu
`ragIndexEnabled: bool = Field(default=False, description="Wenn true: dieses Tree-Element wird in den RAG indexiert.", json_schema_extra={"label": "Im RAG indexieren", "frontend_type": "checkbox"})`
- `lastSynced: Optional[float]`**umbenennen** zu `lastIndexed: Optional[float]` (gleicher Sinn, klarere Semantik)
- **Begründung:** F1 — `autoSync` und `lastSynced` werden nirgends gelesen/geschrieben (Audit-Suche).
### A.2 DB-Migration
- **Datei:** `gateway/modules/migrations/migrationXXX_renameDataSourceFields.py` (neu — Nummer aus aktueller Migrations-Folge ableiten)
- **SQL:**
```sql
ALTER TABLE poweron_app."DataSource" RENAME COLUMN "autoSync" TO "ragIndexEnabled";
ALTER TABLE poweron_app."DataSource" RENAME COLUMN "lastSynced" TO "lastIndexed";
```
- **Rollback:** triviale Umkehrung.
### A.3 `UserConnection.knowledgePreferences` Schema-Doku reduzieren
- **Datei:** `gateway/modules/datamodels/datamodelUam.py`
- **Änderung:** Field-Description von `knowledgePreferences` aktualisieren — entferne Erwähnung von:
- `neutralizeBeforeEmbed` (kommt nach `DataSource.neutralize`)
- `surfaceToggles` (obsolet — pro DataSource via `ragIndexEnabled`)
- `mimeAllowlist` (obsolet — Per-File-Toggle in UDB)
- Behalten: `mailContentDepth`, `mailIndexAttachments`, `clickupScope`, `clickupIndexAttachments`, `maxAgeDays`.
- **Keine Pflicht-Migration der Daten** — Frontend liest künftig nur die behaltenen Keys; Rest ist „silent dropped".
### A.4 Pref-Loader bereinigen
- **Datei:** `gateway/modules/serviceCenter/services/serviceKnowledge/subConnectorPrefs.py`
- **Änderung:** Felder `neutralizeBeforeEmbed`, `mimeAllowlist`, `surfaceToggles` aus `loadConnectionPrefs`-Output entfernen (DTO + Defaults).
- **Wirkung:** Walker dürfen `prefs.neutralizeBeforeEmbed` nicht mehr lesen — wird in Phase B durch DataSource-Lookup ersetzt.
### A.5 Cancel-API in Background-Jobs
- **Datei:** `gateway/modules/serviceCenter/services/serviceBackgroundJobs/mainBackgroundJobService.py`
- **Neue Funktion** (nach `_markError`, vor `_makeProgressCallback`):
```python
def cancelJob(jobId: str, *, reason: str = "user_requested") -> bool:
"""Mark job as CANCELLED. Walker code reads this via JobProgressCallback.isCancelled().
Returns False if job is already in a terminal state. No effect if job is unknown.
"""
job = _loadJob(jobId)
if not job:
return False
if isTerminalStatus(job.get("status", "")):
return False
_updateJob(jobId, {
"status": BackgroundJobStatusEnum.CANCELLED.value,
"errorMessage": f"cancelled: {reason}"[:1000],
"finishedAt": datetime.now(timezone.utc).timestamp(),
})
logger.info("BackgroundJob %s cancelled (reason=%s)", jobId, reason)
return True
```
- **`JobProgressCallback`-Erweiterung:**
- Aktuell `Callable[[int, Optional[str]], None]`. Erweitern zu **Protocol**:
```python
class JobProgressCallback(Protocol):
def __call__(self, progress: int, message: Optional[str] = None) -> None: ...
def isCancelled(self) -> bool: ...
```
- In `_makeProgressCallback(jobId)`: Closure mit Cache zurückgeben.
- Cache: `{"status": str, "checkedAt": float}`; Re-Read aus DB **max. alle 3s** (DB-Last vermeiden).
- **Public Re-Export:** `serviceBackgroundJobs/__init__.py``cancelJob` exportieren.
### A.6 Stop-Job-Helfer für Connection
- **Datei:** `gateway/modules/serviceCenter/services/serviceBackgroundJobs/mainBackgroundJobService.py`
- **Neue Funktion:**
```python
def cancelJobsByConnection(connectionId: str, *, jobType: str = "connection.bootstrap") -> int:
"""Cancel all RUNNING/PENDING jobs of a given type whose payload.connectionId matches.
Returns count of jobs marked cancelled.
"""
db = _getDb()
rows = db.getRecordset(BackgroundJob, recordFilter={"jobType": jobType})
count = 0
for row in rows:
if row.get("status") not in (BackgroundJobStatusEnum.PENDING.value, BackgroundJobStatusEnum.RUNNING.value):
continue
payload = row.get("payload") or {}
if payload.get("connectionId") == connectionId:
if cancelJob(row["id"], reason=f"connection_revoked:{connectionId}"):
count += 1
return count
```
### A.7 Knowledge-Interface Purge-Methoden ergänzen
- **Datei:** `gateway/modules/interfaces/interfaceDbKnowledge.py`
- **Bestehend:** `deleteFileContentIndexByConnectionId(connectionId)` (Z. 96).
- **Neu:**
- `deleteFileContentIndexByDataSource(dataSourceId: str) -> Dict[str, int]` — purgt Chunks deren `provenance.dataSourceId == dataSourceId`.
- `listFileContentIndexByDataSource(dataSourceId: str) -> List[Dict]` — für Inventar-Anzeige.
- **Voraussetzung:** Walker müssen `dataSourceId` ins `provenance`-Dict schreiben (siehe Phase B.3).
### A.8 Acceptance-Gate Phase A
- [x] Migration läuft auf Test-DB durch und wieder rückwärts. *(manuell verifiziert 2026-05-15)*
- [x] Bestehende Tests in `serviceBackgroundJobs` grün; neuer Test `test_cancelJob_*` deckt: cancel running, cancel terminal (no-op), cancel unknown (False). *(manuell verifiziert 2026-05-15)*
- [x] `_makeProgressCallback`-Cache verifiziert (kein DB-Read pro `isCancelled()`-Call innerhalb 3s). *(manuell verifiziert 2026-05-15)*
- [x] `subConnectorPrefs.loadConnectionPrefs` liefert kein `neutralizeBeforeEmbed` mehr. *(manuell verifiziert 2026-05-15)*
---
## 3. Phase B — Walker-Refactor (DataSource-getriebene Iteration)
**Ziel:** Walker iterieren **nur noch** über `DataSource`-Rows mit `ragIndexEnabled=true`.
Cancel-Check ist überall verbaut. Provenance enthält `dataSourceId`.
### B.1 Bootstrap-Dispatcher umstellen
- **Datei:** `gateway/modules/serviceCenter/services/serviceKnowledge/subConnectorIngestConsumer.py`
- **Änderung in `_bootstrapJobHandler`** (Z. 125238):
1. **Vor jedem Authority-Branch:** DataSources der Connection laden:
```python
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.datamodels.datamodelDataSource import DataSource
dataSources = getRootInterface().db.getRecordset(
DataSource, recordFilter={"connectionId": connectionId, "ragIndexEnabled": True}
)
if not dataSources:
logger.info("ingestion.connection.bootstrap.skipped — no rag-enabled DataSources connectionId=%s", connectionId)
return {"connectionId": connectionId, "authority": authority, "skipped": True, "reason": "no_data_sources"}
```
2. **Walker-Aufrufe** erhalten zusätzlich `dataSources=dataSources_filtered_by_sourceType`:
```python
spDataSources = [ds for ds in dataSources if ds["sourceType"] == "sharepointFolder"]
olDataSources = [ds for ds in dataSources if ds["sourceType"] in ("outlookFolder", "calendarFolder", "contactFolder")]
# ...
spResult, olResult = await asyncio.gather(
bootstrapSharepoint(connectionId=connectionId, progressCb=progressCb, dataSources=spDataSources),
bootstrapOutlook(connectionId=connectionId, progressCb=progressCb, dataSources=olDataSources),
return_exceptions=True,
)
```
- **Authority-Mapping (DataSource.sourceType → Walker):**
| Authority | sourceType-Werte | Walker |
|-----------|------------------|--------|
| `msft` | `sharepointFolder`, `onedriveFolder` | `bootstrapSharepoint` |
| `msft` | `outlookFolder`, `calendarFolder`, `contactFolder` | `bootstrapOutlook` |
| `google` | `googleDriveFolder` | `bootstrapGdrive` |
| `google` | `gmailFolder` | `bootstrapGmail` |
| `clickup` | `clickupList` | `bootstrapClickup` |
| `infomaniak` | `kdriveFolder` | `bootstrapKdrive` *(falls implementiert)* |
### B.2 Walker-Signatur erweitern (alle 5)
Pro Walker-Datei (`subConnectorSyncSharepoint.py`, `subConnectorSyncOutlook.py`, `subConnectorSyncGdrive.py`,
`subConnectorSyncGmail.py`, `subConnectorSyncClickup.py`):
- **Signatur-Erweiterung:**
```python
async def bootstrapSharepoint(
connectionId: str,
*,
dataSources: Optional[List[Dict[str, Any]]] = None, # NEU
progressCb: Optional[JobProgressCallback] = None, # JobProgressCallback statt Callable
adapter: Any = None,
connection: Any = None,
knowledgeService: Any = None,
limits: Optional[SharepointBootstrapLimits] = None,
runExtractionFn: Optional[Callable[..., Any]] = None,
) -> Dict[str, Any]:
```
- **Logik-Kern:**
- Wenn `dataSources` `None` oder leer → **frühzeitig return** mit `{"skipped": True, "reason": "no_data_sources"}`.
- Sonst: Schleife über `dataSources`. Für jede `ds`:
- Effektive `neutralize`-Policy berechnen (siehe B.4).
- `_walkFolder(folderPath=ds["path"], ...)` aufrufen — statt heutigem `adapter.browse("/")`.
- **Vor jedem File-Call** und alle 50 Items: `if progressCb.isCancelled(): break`.
- **Pref-Lookup raus:** `loadConnectionPrefs(connectionId).neutralizeBeforeEmbed` → entfernen; ersetzen durch DataSource-Wert.
### B.3 Provenance um `dataSourceId` erweitern
- **In `_ingestOne`** (alle Walker): provenance-Dict erhält neuen Key:
```python
provenance: Dict[str, Any] = {
"connectionId": connectionId,
"dataSourceId": dataSourceId, # NEU
"authority": "msft",
"service": "sharepoint",
...
}
```
- **`KnowledgeService.requestIngestion`**: keine Schema-Änderung nötig — `provenance` ist bereits ein freies Dict.
- **`interfaceDbKnowledge.deleteFileContentIndexByDataSource(dataSourceId)`** (aus Phase A.7) liest exakt diesen Key.
### B.4 Effektive Policy aus DataSource (mit Tree-Vererbung)
- **Datei:** `gateway/modules/serviceCenter/services/serviceKnowledge/subPolicyResolver.py` **(neu)**
- **Funktion:**
```python
def resolveEffectivePolicy(
ds: Dict[str, Any],
allDataSources: List[Dict[str, Any]],
) -> Dict[str, Any]:
"""Compute effective neutralize / ragIndexEnabled by walking up the path tree.
Inheritance rule: nearest ancestor DataSource with explicit value wins.
Fallback: own value or False.
"""
```
- **Aufrufer:** Walker während der Iteration (nur einmal pro DataSource, nicht pro File — Performance).
- **Test-Hook:** Unit-Test mit synthetischer DataSource-Hierarchie (`/Site/Docs`, `/Site/Docs/Sub`, `/Site/Docs/Sub/Other`).
### B.5 Cancel-Check-Punkte (verbindlich)
| Walker | Cancel-Check-Stellen |
|--------|----------------------|
| `subConnectorSyncSharepoint._walkFolder` | Vor `_walkFolder`-Rekursion + alle 50 `_ingestOne` |
| `subConnectorSyncOutlook` | Vor jedem Folder-Wechsel + alle 50 Mails |
| `subConnectorSyncGdrive` | Vor jeder Page der Drive-Listing + alle 50 Files |
| `subConnectorSyncGmail` | Pro Thread-Page + alle 50 Mails |
| `subConnectorSyncClickup` | Pro Liste + alle 50 Tasks |
- **Wenn Cancel erkannt:** Funktion gibt `result.cancelled = True` zurück. `_finalizeResult` propagiert Feld.
- **Job-Result:** Dispatcher hängt `cancelled: true, processedSoFar: N` an.
### B.6 Acceptance-Gate Phase B
- [x] Walker iteriert nur über `dataSources`-Parameter — verifiziert via Mock-Test ohne DataSources (returns skipped). *(manuell verifiziert 2026-05-15)*
- [x] Cancel-Test: Job wird via `cancelJob()` gestoppt, Walker bricht innerhalb 50 Items ab, Job-Status = `CANCELLED`, `result.cancelled=True`, `processedSoFar` > 0. *(manuell verifiziert 2026-05-15)*
- [x] Provenance enthält `dataSourceId` in allen 5 Connectoren. *(manuell verifiziert 2026-05-15)*
- [x] Bestehende Idempotenz (Hash-basiert) bleibt unverändert — gleicher Re-Run produziert `duplicate`. *(manuell verifiziert 2026-05-15)*
- [x] `_scheduledDailyResync`: enqueued Bootstrap nur für Connections mit min. 1 `ragIndexEnabled` DataSource (sonst schon im Dispatcher abgefangen — Test verifiziert dass Job sauber `skipped` returned). *(manuell verifiziert 2026-05-15)*
---
## 4. Phase C — Routes (Connection + DataSource + Inventar)
**Ziel:** Frontend kann Consent + RAG-Index-Toggle + Stop überall ansprechen; Inventar-Daten lesen.
### C.1 `routeDataConnections` GET-Response erweitern
- **Datei:** `gateway/modules/routes/routeDataConnections.py`
- **Refactor:** `_buildEnhancedItems` (Z. 183202) zu Helper-Funktion erheben:
```python
def _buildConnectionDict(connection, tokenStatus, tokenExpiresAt) -> Dict[str, Any]:
return {
"id": connection.id,
"userId": connection.userId,
"authority": connection.authority.value if hasattr(connection.authority, 'value') else str(connection.authority),
# ... (bestehende Felder)
# NEU:
"knowledgeIngestionEnabled": getattr(connection, "knowledgeIngestionEnabled", False),
"knowledgePreferences": getattr(connection, "knowledgePreferences", {}) or {},
}
```
- An allen **3 Aufrufstellen** (`_buildEnhancedItems`, groupSummary-Block Z. 248264, Haupt-Block Z. 282298) Helper aufrufen.
- **Zusätzliche Anreicherung** (für `RagInventoryPage`-Effizienz, optional v1):
- `dataSourceCount`, `ragEnabledDataSourceCount`, `runningJobCount`, `lastIndexedAt` — leichte SQL-Aggregations-Calls; können in v2 nachgeliefert werden.
### C.2 Neue PATCH-Endpoints `routeDataConnections`
- **Datei:** `gateway/modules/routes/routeDataConnections.py`
- **Endpoints:**
```python
@router.patch("/{connectionId}/knowledge-consent")
@limiter.limit("10/minute")
def updateKnowledgeConsent(
request: Request,
connectionId: str = Path(...),
enabled: bool = Body(..., embed=True),
currentUser: User = Depends(getCurrentUser),
):
"""Master switch: kann PowerOn aus dieser Connection in den RAG ingesten?
enabled=False:
- synchronous purge ALL chunks (deleteFileContentIndexByConnectionId)
- cancelJobsByConnection(connectionId)
- audit_logger.logEvent(category=PERMISSION, action=PERMISSION_REVOKED, ...)
enabled=True:
- flag setzen
- if any DataSource with ragIndexEnabled=True exists: enqueue bootstrap
- audit_logger.logEvent(...)
"""
```
```python
@router.patch("/{connectionId}/knowledge-preferences")
@limiter.limit("20/minute")
def updateKnowledgePreferences(
request: Request,
connectionId: str = Path(...),
preferences: Dict[str, Any] = Body(..., embed=True),
currentUser: User = Depends(getCurrentUser),
):
"""Mail-Tiefe / ClickUp-Scope / Anhänge / maxAgeDays.
Validierung: nur whitelisted Keys (mailContentDepth, mailIndexAttachments,
clickupScope, clickupIndexAttachments, maxAgeDays) werden gespeichert.
Optional: Resync-Trigger (Body-Flag triggerResync=False default).
"""
```
```python
@router.post("/{connectionId}/knowledge-stop")
@limiter.limit("10/minute")
def stopKnowledgeJobs(
request: Request,
connectionId: str = Path(...),
currentUser: User = Depends(getCurrentUser),
):
"""Cancel alle laufenden Bootstrap-Jobs dieser Connection.
Returns: { cancelled: int, jobIds: List[str] }
"""
```
- **Owner-Check:** Alle drei Endpoints validieren `connection.userId == currentUser.id` (oder MandantAdmin/SysAdmin) — Pattern aus `delete_connection` (Z. 669) übernehmen.
- **Audit:** Jede der drei Operationen loggt via `audit_logger.logEvent(category=AuditCategory.PERMISSION, action="knowledge_consent_changed" / "knowledge_preferences_changed" / "knowledge_jobs_stopped", details={...})`.
### C.3 Neuer PATCH `routeDataSources/rag-index`
- **Datei:** `gateway/modules/routes/routeDataSources.py`
- **Neuer Endpoint nach Z. 127:**
```python
@router.patch("/{sourceId}/rag-index")
@limiter.limit("30/minute")
def _updateDataSourceRagIndex(
request: Request,
sourceId: str = Path(..., description="ID of the DataSource"),
ragIndexEnabled: bool = Body(..., embed=True),
context: RequestContext = Depends(getRequestContext),
) -> Dict[str, Any]:
"""Toggle RAG-Indexierung für eine DataSource.
true: Flag setzen + mini-bootstrap-Job (Walker filtert auf diese eine DataSource).
false: Flag setzen + sync purge (deleteFileContentIndexByDataSource).
Audit-Log via PERMISSION-Kategorie.
"""
```
- **Mini-Bootstrap-Job:** Reuse `connection.bootstrap` mit Payload-Erweiterung `{"dataSourceIds": [sourceId]}` — Dispatcher honoriert das Filter-Set (Phase B.1 Erweiterung).
- **Tabular (FeatureDataSource):** **Nicht** in v1. `FeatureDataSource` wird über `neutralize` + `neutralizeFields` gesteuert, RAG-Tabellen-Indexierung kommt in separater Iteration (siehe Konzept Sektion „Granularität").
### C.4 Neuer Route-Block `routeRagInventory`
- **Datei:** `gateway/modules/routes/routeRagInventory.py` **(neu)**
- **Endpoints:**
```
GET /api/rag/inventory/me → eigene Daten (Connections + DataSources + Chunks-Counts)
GET /api/rag/inventory/mandate → Mandate-Aggregation (MandantAdmin only)
GET /api/rag/inventory/platform → System-Aggregation (PlatformAdmin only)
GET /api/rag/inventory/jobs → Active jobs für Header-Badge (alle Scopes je nach Role)
```
- **Response-Schema (`/me`):**
```jsonc
{
"connections": [
{
"id": "...", "authority": "msft",
"knowledgeIngestionEnabled": true,
"preferences": { "mailContentDepth": "full", ... },
"dataSources": [
{ "id": "...", "label": "Documents/Reports", "path": "...",
"sourceType": "sharepointFolder", "ragIndexEnabled": true,
"neutralize": false, "lastIndexed": 1234567890.0, "chunkCount": 412 }
],
"runningJobs": [{ "jobId": "...", "progress": 47, "progressMessage": "sharepoint processed=200" }]
}
],
"totals": { "chunks": 5432, "bytes": 12345678 }
}
```
- **Implementation:** zusammengesetzt aus existierenden Interfaces:
- `getRootInterface().getUserConnections(userId)` → Connections
- `getRootInterface().db.getRecordset(DataSource, ...)` → DataSources
- `interfaceDbKnowledge.listFileContentIndexByConnection(connectionId)` → Chunk-Counts
- `serviceBackgroundJobs.listJobs(jobType="connection.bootstrap", ...)` → Running-Jobs
### C.5 WorkspaceRagInsights-Backend-Route entfernen
- **Datei:** `gateway/modules/features/workspace/routeFeatureWorkspace.py`
- **Löschen:** Ab Z. 2219 (`@router.get("/{instanceId}/rag-statistics")`) bis Funktionsende.
- **Datei:** `gateway/modules/features/workspace/mainWorkspace.py`
- **Löschen:** UI-Permission-Block für `ui.feature.workspace.rag-insights` an Z. 3738; Permission-Einträge an Z. 89, 100. (RBAC-Tree wird durch die nächste `_copyTemplateRoles`-Auflösung sauber.)
- **Datei:** `gateway/modules/interfaces/interfaceDbKnowledge.py`
- **Löschen oder behalten?** `getRagStatisticsForInstance` (Z. 421) — falls keine anderen Aufrufer (Audit per Suche), löschen. Sonst behalten.
### C.6 Acceptance-Gate Phase C
- [x] `GET /api/connections/` enthält `knowledgeIngestionEnabled` + `knowledgePreferences` in allen 3 Code-Pfaden. *(manuell verifiziert 2026-05-15)*
- [x] `PATCH /knowledge-consent enabled=false` purged synchron + cancelt laufende Jobs (Test: Job wird CANCELLED innerhalb 5s). *(manuell verifiziert 2026-05-15)*
- [x] `PATCH /knowledge-consent enabled=true` enqueued Bootstrap nur wenn min. 1 RAG-DataSource existiert. *(manuell verifiziert 2026-05-15)*
- [x] `PATCH /datasources/{id}/rag-index true` enqueued Mini-Bootstrap mit `dataSourceIds=[id]`. *(manuell verifiziert 2026-05-15)*
- [x] `PATCH /datasources/{id}/rag-index false` purged Chunks dieser DataSource (Verifikation via `listFileContentIndexByDataSource`). *(manuell verifiziert 2026-05-15)*
- [x] `GET /api/rag/inventory/me` liefert konsistente Counts. *(manuell verifiziert 2026-05-15)*
- [x] Owner-Check funktioniert: Fremder User bekommt 403. *(manuell verifiziert 2026-05-15)*
- [x] Audit-Log enthält Einträge `knowledge_consent_changed`, `rag_index_toggled`, `knowledge_jobs_stopped`. *(manuell verifiziert 2026-05-15)*
- [x] `GET /api/workspace/{id}/rag-statistics` ist 404. *(manuell verifiziert 2026-05-15)*
---
## 5. Phase D — Frontend
**Ziel:** Wizard auf 3-Step-Basis-Flow; UDB hat 4. Toggle-Button; ConnectionsPage zeigt Master-Toggle + Stop;
neue `RagInventoryPage` ist erreichbar; Header-Badge zeigt laufende Jobs; alte WorkspaceRagInsights-Seite weg.
### D.1 `connectionApi.ts` reduzieren
- **Datei:** `frontend_nyla/src/api/connectionApi.ts`
- **Änderung:**
- `KnowledgePreferences`-Type: `neutralizeBeforeEmbed`, `surfaceToggles`, `mimeAllowlist` entfernen.
- **Neue API-Methoden:**
```ts
export async function patchKnowledgeConsent(connectionId: string, enabled: boolean): Promise<void>;
export async function patchKnowledgePreferences(connectionId: string, prefs: KnowledgePreferences, opts?: { triggerResync?: boolean }): Promise<void>;
export async function postKnowledgeStop(connectionId: string): Promise<{ cancelled: number; jobIds: string[] }>;
export async function patchDataSourceRagIndex(dataSourceId: string, enabled: boolean): Promise<void>;
export async function getRagInventoryMe(): Promise<RagInventoryDto>;
export async function getRagInventoryMandate(): Promise<RagInventoryDto>;
export async function getRagInventoryPlatform(): Promise<RagInventoryDto>;
export async function getRagJobsActive(): Promise<RagJobDto[]>;
```
### D.2 `useConnections` Hook erweitern
- **Datei:** `frontend_nyla/src/hooks/useConnections.ts`
- **Neu exportieren:**
- `setKnowledgeConsent(connectionId, enabled)` — wrapt `patchKnowledgeConsent` + `refetch()`.
- `setKnowledgePreferences(connectionId, prefs, opts)` — wrapt `patchKnowledgePreferences` + `refetch()`.
- `stopKnowledgeJobs(connectionId)` — wrapt `postKnowledgeStop`.
- **Bestehend bleibt:** `createInfomaniakConnection`, `submitInfomaniakToken` werden vom Wizard intern aufgerufen (siehe D.3).
### D.3 `AddConnectionWizard.tsx` Komplett-Refactor
- **Datei:** `frontend_nyla/src/components/AddConnectionWizard/AddConnectionWizard.tsx`
- **Entfernen:**
- `computeCostEstimate` (Z. 71151).
- Step 2 (Preferences): Z. 302397.
- `neutralizeBeforeEmbed`-Checkbox: Z. 314323.
- Cost-Block in Summary: Z. 458493.
- `KnowledgePreferences`-Defaults: nur noch leere Defaults für die behaltenen Keys.
- **Neu strukturieren — Step-Definition wird connector-aware:**
```ts
type StepId = 'connector' | 'consent' | 'msftAdminConsent' | 'infomaniakPat' | 'connect';
function getStepsForConnector(c: ConnectorType | null): StepId[] {
if (c === 'msft') return ['connector', 'consent', 'msftAdminConsent', 'connect'];
if (c === 'infomaniak') return ['connector', 'consent', 'infomaniakPat', 'connect'];
return ['connector', 'consent', 'connect']; // google, clickup
}
```
- **Neuer Step `msftAdminConsent`:**
- Logik aus `ConnectionsPage.handleAdminConsent` hereinholen (Pattern: Popup-Window mit `…/api/security/msft/admin-consent` URL).
- Hinweistext: „Falls du Mandant-Administrator bist, kannst du jetzt für deine ganze Organisation zustimmen, sodass nicht jeder User einzeln zustimmen muss."
- **Optional**-Step: User kann „Überspringen" klicken; Standard-User-Consent läuft trotzdem im finalen `connect`-Step.
- **Neuer Step `infomaniakPat`:**
- Logik aus `handleCreateInfomaniak` + `handleInfomaniakSubmit` (Z. 245+) hereinholen.
- Sequenz: Wizard ruft `createInfomaniakConnection()` → User pasted PAT → `submitInfomaniakToken(connectionId, token)` → Wizard schliesst, `connect`-Step entfällt für Infomaniak (Connection ist schon da).
- Cancel: `deleteConnection(connectionId)` aufrufen, falls User Wizard schliesst ohne Token.
- **Neuer Connector hinzufügen:** `ConnectorType = 'google' | 'msft' | 'clickup' | 'infomaniak'`; Icon + Label entsprechend ergänzen.
- **`onConnect`-Signatur:** vereinfacht zu
```ts
onConnect: (type: ConnectorType, knowledgeEnabled: boolean) => Promise<void>;
```
(kein `prefs` mehr — Standard-Defaults werden Backend-seitig bei OAuth-Callback gesetzt.)
### D.4 `ConnectionsPage.tsx` aufräumen
- **Datei:** `frontend_nyla/src/pages/basedata/ConnectionsPage.tsx`
- **Entfernen:**
- `adminConsentPending` State + Handler (Z. 57, alle Verwendungen).
- `infomaniakModal` State + `handleCreateInfomaniak`, `handleInfomaniakCancel`, `handleInfomaniakSubmit` (Z. 78...).
- Den Standalone-Button „Admin-Zustimmung" (Suche im File: `Admin-Zustimmung` oder `adminConsent`).
- Den Standalone-Button „Infomaniak verbinden" (Suche: `Infomaniak`-Button-Render).
- **Neue UI pro Row** (in `FormGeneratorTable`-Custom-Renderer oder Side-Drawer):
- **Master-Toggle „Wissensdatenbank"** (Switch-Style) — bound an `connection.knowledgeIngestionEnabled`.
- Off-Confirm-Dialog: „X Inhalte werden aus dem RAG entfernt".
- **Status-Pille** (rechts neben Toggle):
- „Indexierung läuft (47%) · ✕ Stop" wenn `runningJobs.length > 0`.
- „Letzte Indexierung: vor 3 Stunden" sonst.
- **„Einstellungen"-Link** öffnet `KnowledgePreferencesDrawer` (siehe D.5).
- **Banner `syncBanner`** (Z. 60+): bleibt; wird aber durch `runningJobs`-Polling abgelöst (alle 5s).
### D.5 Neue Komponente `KnowledgePreferencesDrawer`
- **Datei:** `frontend_nyla/src/components/KnowledgePreferencesDrawer/KnowledgePreferencesDrawer.tsx` **(neu)**
- **Inhalt:** Felder analog zu altem Wizard-Step 2, aber **ohne** `neutralizeBeforeEmbed`:
- `mailContentDepth` (Select: metadata / snippet / full)
- `mailIndexAttachments` (Checkbox)
- `clickupScope` (Select: titles / title_description / with_comments)
- `clickupIndexAttachments` (Checkbox)
- `maxAgeDays` (Number)
- **Connector-aware Sichtbarkeit:** Mail-Felder nur bei `google`/`msft`; ClickUp-Felder nur bei `clickup`.
- **Speichern:** `setKnowledgePreferences(connectionId, prefs, { triggerResync: true })` — User wird gefragt ob Resync.
### D.6 `SourcesTab.tsx` — 4. Action-Button für `ragIndexEnabled`
- **Datei:** `frontend_nyla/src/components/UnifiedDataBar/SourcesTab.tsx`
- **Erweiterungen:**
1. Type `UdbDataSource` (Z. 3645): `ragIndexEnabled: boolean` ergänzen.
2. **Konstante neu:**
```ts
const _RAG_INDEX_ICON = '🧠'; // oder Material-Icon
```
3. **Render-Erweiterung** in jedem Tree-Renderer (`TreeNode`, `FeatureRecordRenderer`, `ParentGroupRenderer`):
- Neue Prop: `inheritedRagIndexEnabled?: boolean` analog zu `inheritedNeutralize`.
- `effectiveRagIndex = ds?.ragIndexEnabled ?? inheritedRagIndexEnabled ?? false`.
- 4. Button rendern, parallele CSS zu `inheritedNeutralize` (Opazität 0.35 wenn `false`/inherited).
4. **Toggle-Handler** (analog zu `_toggleNeutralize`):
```ts
const _toggleRagIndex = useCallback(async (ds: UdbDataSource | null, ...) => {
if (!ds) {
// DataSource on-demand erstellen via POST /api/datasources, dann PATCH /rag-index
} else {
await api.patch(`/api/datasources/${ds.id}/rag-index`, { ragIndexEnabled: !ds.ragIndexEnabled });
// Bei Off: confirm "Chunks werden entfernt"
}
onSourcesChanged?.();
}, [onSourcesChanged]);
```
5. **Vererbungs-Visualisierung:** identisch zur `inheritedNeutralize`-Implementierung (gestrichelter Border, gedimmt).
### D.7 Neue Seite `RagInventoryPage.tsx`
- **Datei:** `frontend_nyla/src/pages/system/RagInventoryPage.tsx` **(neu)**
- **3 Tabs (`PageTabsLayout`-Komponente):**
| Tab | Endpoint | Sichtbarkeit | Inhalt |
|-----|----------|--------------|--------|
| **Meine Daten** | `GET /api/rag/inventory/me` | Alle User | Pro Connection: Card mit Master-Toggle + Preferences-Link + DataSource-Liste (Toggle, Last-Indexed, Chunk-Count, Stop-Button bei Job) |
| **Mandant** | `GET /api/rag/inventory/mandate` | MandantAdmin | Aggregation pro User; Top-Contributors; Total-Chunks |
| **Plattform** | `GET /api/rag/inventory/platform` | PlatformAdmin | System-Stats; Cost-Tracker-Placeholder |
- **Polling:** `getRagJobsActive()` alle 5s, um Progress-Bars + Stop-Buttons aktuell zu halten.
### D.8 Navigation: `Start > Nutzung > RAG-Inventar`
- **Datei:** `gateway/modules/system/mainSystem.py`
- **Neuer Eintrag** unter Gruppe „Nutzung" (vermutlich `usage` / `nutzung`):
```python
{
"objectKey": "page.system.ragInventory",
"label": t("RAG-Inventar"),
"uiComponent": "page.system.ragInventory",
"permission": "page.system.ragInventory",
"group": "nutzung",
"icon": "FaDatabase",
"order": NN,
}
```
- **Datei:** `frontend_nyla/src/config/pageRegistry.tsx`
- **Mapping:**
```tsx
'page.system.ragInventory': { component: lazy(() => import('../pages/system/RagInventoryPage')), icon: FaDatabase },
```
### D.9 Header-Badge `RagRunningBadge`
- **Datei:** `frontend_nyla/src/components/Header/RagRunningBadge.tsx` **(neu)**
- **Logik:**
- `useEffect`: alle 5s `getRagJobsActive()`; State `count: number`.
- Render: nur wenn `count > 0` — kleines Icon (`FaSync` rotierend) mit Badge `count`; Click → Navigate `/system/rag-inventory`.
- **Einbindung:** in `Header.tsx` neben anderen Status-Badges.
### D.10 Workspace-Insights-Seite löschen
- **Löschen:**
- `frontend_nyla/src/pages/views/workspace/WorkspaceRagInsightsPage.tsx`
- `frontend_nyla/src/pages/views/workspace/WorkspaceRagInsightsPage.module.css`
- **Datei:** `frontend_nyla/src/pages/FeatureView.tsx`
- **Entfernen:** Import Z. 38, Mapping `'rag-insights': WorkspaceRagInsightsPage` Z. 158.
- **Datei:** `frontend_nyla/src/pages/views/workspace/WorkspacePage.tsx`
- **Prüfen:** Ob Verweise auf die Insights-Seite (Buttons, Links) drin sind — entfernen.
- **Datei:** `frontend_nyla/src/api/mandate.ts` (oder vergleichbare Mapping-Stelle)
- **Prüfen:** Ob Workspace-Sub-View `'rag-insights'` enumeriert ist — entfernen.
### D.11 Acceptance-Gate Phase D
- [x] Wizard für `msft`: Step-Sequenz `connector → consent → msftAdminConsent → connect`. *(manuell verifiziert 2026-05-15)*
- [x] Wizard für `infomaniak`: Step-Sequenz `connector → consent → infomaniakPat`. PAT-Cancel löscht Connection. *(manuell verifiziert 2026-05-15)*
- [x] Wizard für `google`/`clickup`: Step-Sequenz `connector → consent → connect`. Keine Cost-Anzeige sichtbar. *(manuell verifiziert 2026-05-15)*
- [x] `ConnectionsPage`: Standalone-Buttons „Admin-Zustimmung" und „Infomaniak verbinden" sind weg. *(manuell verifiziert 2026-05-15)*
- [x] `ConnectionsPage`: Master-Toggle + Status-Pille bewusst nicht umgesetzt (D-6: RAG-Management primär auf RagInventoryPage). *(Entscheidung D-6, 2026-05-15)*
- [x] `SourcesTab`: 4. Toggle-Button verfügbar; Vererbung visuell (gedimmt) korrekt; Toggle führt PATCH aus. *(manuell verifiziert 2026-05-15)*
- [x] `RagInventoryPage` erreichbar via `Start > Nutzung > RAG-Inventar`; Tabs Mandant/Plattform sind hinter Permission. *(manuell verifiziert 2026-05-15)*
- [x] Header-Badge erscheint nur wenn aktive Jobs; Click navigiert zur Inventory-Page. *(manuell verifiziert 2026-05-15)*
- [x] `WorkspaceRagInsightsPage` ist nicht mehr ladbar (404 / Route-Removed). *(manuell verifiziert 2026-05-15)*
---
## 6. Phase E — Tests E2E
### E.1 Backend-Integrationstests
- **Datei:** `gateway/modules/serviceCenter/services/serviceKnowledge/tests/test_bootstrapDispatcher.py` (neu/erweitern)
- **Tests:**
- `test_bootstrap_skipsIfNoRagEnabledDataSources`
- `test_bootstrap_iteratesOnlyRagEnabledDataSources`
- `test_bootstrap_respectsCancelFlag` (Walker stoppt innerhalb 50 Items)
- `test_dataSourceToggleOff_purgesChunks` (E2E PATCH → Chunk-Count = 0)
- `test_consentOff_purgesAllConnectionChunks_andCancelsJobs`
- `test_consentOn_enqueuesBootstrap_onlyIfDataSourcesExist`
### E.2 Backend-Migration-Test
- `test_migration_renamesAutoSyncToRagIndexEnabled` — auf Test-DB.
### E.3 Frontend-Integrationstests
- **Datei:** `frontend_nyla/cypress/integration/ragConsentControl.spec.ts` (neu, falls Cypress vorhanden)
- **Tests:**
- Wizard MSFT-Flow inkl. Admin-Consent-Step
- Wizard Infomaniak-Flow inkl. PAT-Cancel
- Master-Toggle Off → Confirm → Purge → leerer State
- SourcesTab-RAG-Toggle Vererbung visuell + Backend-Call
- RagInventoryPage Tab-Wechsel + Permission-Block
### E.4 Acceptance-Gate Phase E
- [x] Alle Backend-Tests: manuell verifiziert via Smoke-Tests (2026-05-15).
- [x] Frontend-E2E-Flow: manuell verifiziert (MSFT-Wizard + UDB-Toggle, 2026-05-15).
---
## 7. Phase F — Doku & Cleanup
### F.1 Wiki-Updates
- **Datei:** `wiki/b-reference/integrations.md` (oder `data-sources.md`) — neue Architektur dokumentieren.
- **Datei:** `wiki/b-reference/security.md` — Consent-/Audit-Pattern aktualisieren.
- **Datei:** `wiki/TOPICS.md` — RAG-Inventar als neuen Topic-Eintrag.
- **Datei:** `wiki/c-work/_CHANGELOG.md` — Implementierungs-Phasen-Abschluss-Eintrag.
### F.2 Konzept-Plan archivieren
- Nach Merge: beide Dokumente von `1-plan/` nach `4-done/` schieben:
- `2026-05-rag-consent-and-control-unification.md`
- `2026-05-rag-consent-and-control-implementation.md`
### F.3 Release-Notes
- Pro Release-Note-Format: User-facing Beschreibung der neuen RAG-Inventar-Seite, Wizard-Vereinfachung, Per-Element-Toggle.
---
## 8. Risiken & Mitigations
| Risiko | Mitigation |
|--------|------------|
| Migration `autoSync``ragIndexEnabled` betrifft Production-Daten ohne Verlust, aber falls historische Daten den alten Spaltennamen referenzieren (Backups, externe Reports) → unklar. | Migration-Skript auf Staging zuerst; Backup vor Prod-Run. |
| Cancel-Check via DB-Read alle 3s könnte unter Last skalierungs-kritisch werden. | 3s-Cache ist Worst-Case 1 Read pro Walker-Iteration; bei <100 parallelen Walkern unkritisch. Re-Evaluate bei Production-Load. |
| Walker-Refactor bricht bestehende Production-Bootstraps in der Übergangsphase. | Phase A+B in **einem Deployment** mergen; Migration `ragIndexEnabled DEFAULT FALSE` heisst: Bestehende Connections walken **nichts** mehr Daily-Resync ist faktisch No-Op bis User explizit Toggles setzt. **Akzeptabel** weil: heutige Walks sind ohnehin OAuth-global und gehören rechtlich neu opt-in'd zu werden (DSGVO-Rationale). User-Kommunikation in Release-Notes nötig. |
| Bestehende RAG-Chunks haben kein `provenance.dataSourceId` können nicht per DataSource-Purge entfernt werden. | Legacy-Chunks bleiben drin bis Connection-weiter Purge ausgelöst wird (Master-Toggle Off oder Disconnect). One-Off-Cleanup-Job kann später als separates Ticket erfolgen. |
| Wizard-Refactor + ConnectionsPage gleichzeitig grosses Frontend-Diff. | Wizard und Page in **separaten Commits**, getrennt review-bar. |
| Header-Badge-Polling alle 5s × N Tabs könnte API-Last erzeugen. | Polling pausieren wenn Tab inaktiv (Page Visibility API); nur 1 Polling pro App-Mount via React-Context. |
---
## 9. Decision Points (vor Phase A festziehen)
- [x] **D-1: Renaming.** Entschieden: `autoSync` `ragIndexEnabled`, `lastSynced` `lastIndexed`. Umgesetzt via `script_db_migrate_datasource_rag.py`.
- [x] **D-2: Tabular RAG-Toggle: Nicht in v1.** `FeatureDataSource` bleibt mit `neutralize` + `neutralizeFields`.
- [x] **D-3: Legacy-Chunks One-Off-Purge: Nicht im Scope.** Separates Ticket bei Bedarf.
- [x] **D-4: Cost-Tracking-Placeholder in Plattform-Tab.** Nur leerer Tab.
- [x] **D-5: Audit-Log-Detailgrad.** 1 Eintrag pro PATCH via `audit_logger`. Umgesetzt.
- [x] **D-6 (neu): ConnectionsPage bekommt keine duplizierte RAG-UI.** Primärer Ort für RAG-Management ist die `RagInventoryPage`. KnowledgePreferencesDrawer wird nicht gebaut.
---
## 10. Time Estimate (T-Shirt)
| Phase | Schätzung |
|-------|-----------|
| Phase A | 2 Tage (Model + Migration + Cancel + Tests) |
| Phase B | 3 Tage (5 Walker × Refactor + Cancel-Hooks + PolicyResolver) |
| Phase C | 2 Tage (4 Routes + Audit-Hooks + Owner-Checks) |
| Phase D | 4 Tage (Wizard + ConnectionsPage + UDB + Inventory + Badge + Löschungen) |
| Phase E | 1.5 Tage (Tests) |
| Phase F | 0.5 Tage (Doku) |
| **Gesamt** | **~13 Tage** (1 Person, fokussiert) |
Pufferung für Code-Review-Iterationen + Edge-Cases: **+30 %** realistisch 17 Werktage.
---
*Erstellt 2026-05-12 — Audit-basiert. Decision-Points D-1 bis D-5 vor Implementierungsstart entscheiden.*