--- 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:65–69` | 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, 37–41` | 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:168–221`, `subConnectorSyncSharepoint.py:110–174` | 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:188–201, 249–264, 282–298` (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:43–127` | 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:26–34` | 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:37–38, 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. 71–151), `neutralizeBeforeEmbed`-Checkbox (Z. 314–320), Cost-Tabelle (Z. 458–493). MSFT Admin-Consent + Infomaniak laufen über separate Buttons in `ConnectionsPage.tsx` (Z. 245–263, `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. 1112–1592). 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:349–362` | 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. 125–238): 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. 183–202) 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. 248–264, Haupt-Block Z. 282–298) 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. 37–38; 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; export async function patchKnowledgePreferences(connectionId: string, prefs: KnowledgePreferences, opts?: { triggerResync?: boolean }): Promise; export async function postKnowledgeStop(connectionId: string): Promise<{ cancelled: number; jobIds: string[] }>; export async function patchDataSourceRagIndex(dataSourceId: string, enabled: boolean): Promise; export async function getRagInventoryMe(): Promise; export async function getRagInventoryMandate(): Promise; export async function getRagInventoryPlatform(): Promise; export async function getRagJobsActive(): Promise; ``` ### 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. 71–151). - Step 2 (Preferences): Z. 302–397. - `neutralizeBeforeEmbed`-Checkbox: Z. 314–323. - Cost-Block in Summary: Z. 458–493. - `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; ``` (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. 36–45): `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.*