wiki/c-work/4-done/2026-05-rag-consent-and-control-implementation.md
2026-06-02 09:42:12 +02:00

41 KiB
Raw Blame History

title status completed owner relatedConcept created lastReviewed
RAG Consent & Control Implementierungsplan 4-done 2026-05-15 ida ./2026-05-rag-consent-and-control-unification.md 2026-05-12 2026-05-15

RAG Consent & Control — Implementierungsplan

Lese-Kontext: Architektonisches Konzept und Begründungen siehe Schwesterdokument 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 platform-core, ui-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: platform-core/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: platform-core/modules/migrations/migrationXXX_renameDataSourceFields.py (neu — Nummer aus aktueller Migrations-Folge ableiten)
  • 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: platform-core/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: platform-core/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: platform-core/modules/serviceCenter/services/serviceBackgroundJobs/mainBackgroundJobService.py
  • Neue Funktion (nach _markError, vor _makeProgressCallback):
    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:
      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__.pycancelJob exportieren.

A.6 Stop-Job-Helfer für Connection

  • Datei: platform-core/modules/serviceCenter/services/serviceBackgroundJobs/mainBackgroundJobService.py
  • Neue Funktion:
    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: platform-core/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

  • Migration läuft auf Test-DB durch und wieder rückwärts. (manuell verifiziert 2026-05-15)
  • 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)
  • _makeProgressCallback-Cache verifiziert (kein DB-Read pro isCancelled()-Call innerhalb 3s). (manuell verifiziert 2026-05-15)
  • 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: platform-core/modules/serviceCenter/services/serviceKnowledge/subConnectorIngestConsumer.py
  • Änderung in _bootstrapJobHandler (Z. 125238):
    1. Vor jedem Authority-Branch: DataSources der Connection laden:
      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:
      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:
    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:
    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: platform-core/modules/serviceCenter/services/serviceKnowledge/subPolicyResolver.py (neu)
  • Funktion:
    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

  • Walker iteriert nur über dataSources-Parameter — verifiziert via Mock-Test ohne DataSources (returns skipped). (manuell verifiziert 2026-05-15)
  • 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)
  • Provenance enthält dataSourceId in allen 5 Connectoren. (manuell verifiziert 2026-05-15)
  • Bestehende Idempotenz (Hash-basiert) bleibt unverändert — gleicher Re-Run produziert duplicate. (manuell verifiziert 2026-05-15)
  • _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: platform-core/modules/routes/routeDataConnections.py
  • Refactor: _buildEnhancedItems (Z. 183202) zu Helper-Funktion erheben:
    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: platform-core/modules/routes/routeDataConnections.py

  • Endpoints:

    @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(...)
        """
    
    @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).
        """
    
    @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: platform-core/modules/routes/routeDataSources.py
  • Neuer Endpoint nach Z. 127:
    @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: platform-core/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):
    {
      "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: platform-core/modules/features/workspace/routeFeatureWorkspace.py
  • Löschen: Ab Z. 2219 (@router.get("/{instanceId}/rag-statistics")) bis Funktionsende.
  • Datei: platform-core/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: platform-core/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

  • GET /api/connections/ enthält knowledgeIngestionEnabled + knowledgePreferences in allen 3 Code-Pfaden. (manuell verifiziert 2026-05-15)
  • PATCH /knowledge-consent enabled=false purged synchron + cancelt laufende Jobs (Test: Job wird CANCELLED innerhalb 5s). (manuell verifiziert 2026-05-15)
  • PATCH /knowledge-consent enabled=true enqueued Bootstrap nur wenn min. 1 RAG-DataSource existiert. (manuell verifiziert 2026-05-15)
  • PATCH /datasources/{id}/rag-index true enqueued Mini-Bootstrap mit dataSourceIds=[id]. (manuell verifiziert 2026-05-15)
  • PATCH /datasources/{id}/rag-index false purged Chunks dieser DataSource (Verifikation via listFileContentIndexByDataSource). (manuell verifiziert 2026-05-15)
  • GET /api/rag/inventory/me liefert konsistente Counts. (manuell verifiziert 2026-05-15)
  • Owner-Check funktioniert: Fremder User bekommt 403. (manuell verifiziert 2026-05-15)
  • Audit-Log enthält Einträge knowledge_consent_changed, rag_index_toggled, knowledge_jobs_stopped. (manuell verifiziert 2026-05-15)
  • 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: ui-nyla/src/api/connectionApi.ts
  • Änderung:
    • KnowledgePreferences-Type: neutralizeBeforeEmbed, surfaceToggles, mimeAllowlist entfernen.
    • Neue API-Methoden:
      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: ui-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: ui-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:
    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
    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: ui-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: ui-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: ui-nyla/src/components/UnifiedDataBar/SourcesTab.tsx
  • Erweiterungen:
    1. Type UdbDataSource (Z. 3645): ragIndexEnabled: boolean ergänzen.
    2. Konstante neu:
      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.
        1. Button rendern, parallele CSS zu inheritedNeutralize (Opazität 0.35 wenn false/inherited).
    4. Toggle-Handler (analog zu _toggleNeutralize):
      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: ui-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: platform-core/modules/system/mainSystem.py
  • Neuer Eintrag unter Gruppe „Nutzung" (vermutlich usage / nutzung):
    {
        "objectKey": "page.system.ragInventory",
        "label": t("RAG-Inventar"),
        "uiComponent": "page.system.ragInventory",
        "permission": "page.system.ragInventory",
        "group": "nutzung",
        "icon": "FaDatabase",
        "order": NN,
    }
    
  • Datei: ui-nyla/src/config/pageRegistry.tsx
  • Mapping:
    'page.system.ragInventory': { component: lazy(() => import('../pages/system/RagInventoryPage')), icon: FaDatabase },
    

D.9 Header-Badge RagRunningBadge

  • Datei: ui-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:
    • ui-nyla/src/pages/views/workspace/WorkspaceRagInsightsPage.tsx
    • ui-nyla/src/pages/views/workspace/WorkspaceRagInsightsPage.module.css
  • Datei: ui-nyla/src/pages/FeatureView.tsx
  • Entfernen: Import Z. 38, Mapping 'rag-insights': WorkspaceRagInsightsPage Z. 158.
  • Datei: ui-nyla/src/pages/views/workspace/WorkspacePage.tsx
  • Prüfen: Ob Verweise auf die Insights-Seite (Buttons, Links) drin sind — entfernen.
  • Datei: ui-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

  • Wizard für msft: Step-Sequenz connector → consent → msftAdminConsent → connect. (manuell verifiziert 2026-05-15)
  • Wizard für infomaniak: Step-Sequenz connector → consent → infomaniakPat. PAT-Cancel löscht Connection. (manuell verifiziert 2026-05-15)
  • Wizard für google/clickup: Step-Sequenz connector → consent → connect. Keine Cost-Anzeige sichtbar. (manuell verifiziert 2026-05-15)
  • ConnectionsPage: Standalone-Buttons „Admin-Zustimmung" und „Infomaniak verbinden" sind weg. (manuell verifiziert 2026-05-15)
  • ConnectionsPage: Master-Toggle + Status-Pille bewusst nicht umgesetzt (D-6: RAG-Management primär auf RagInventoryPage). (Entscheidung D-6, 2026-05-15)
  • SourcesTab: 4. Toggle-Button verfügbar; Vererbung visuell (gedimmt) korrekt; Toggle führt PATCH aus. (manuell verifiziert 2026-05-15)
  • RagInventoryPage erreichbar via Start > Nutzung > RAG-Inventar; Tabs Mandant/Plattform sind hinter Permission. (manuell verifiziert 2026-05-15)
  • Header-Badge erscheint nur wenn aktive Jobs; Click navigiert zur Inventory-Page. (manuell verifiziert 2026-05-15)
  • WorkspaceRagInsightsPage ist nicht mehr ladbar (404 / Route-Removed). (manuell verifiziert 2026-05-15)

6. Phase E — Tests E2E

E.1 Backend-Integrationstests

  • Datei: platform-core/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: ui-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

  • Alle Backend-Tests: manuell verifiziert via Smoke-Tests (2026-05-15).
  • 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 autoSyncragIndexEnabled 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)

  • D-1: Renaming. Entschieden: autoSyncragIndexEnabled, lastSyncedlastIndexed. Umgesetzt via script_db_migrate_datasource_rag.py.
  • D-2: Tabular RAG-Toggle: Nicht in v1. FeatureDataSource bleibt mit neutralize + neutralizeFields.
  • D-3: Legacy-Chunks One-Off-Purge: Nicht im Scope. Separates Ticket bei Bedarf.
  • D-4: Cost-Tracking-Placeholder in Plattform-Tab. Nur leerer Tab.
  • D-5: Audit-Log-Detailgrad. 1 Eintrag pro PATCH via audit_logger. Umgesetzt.
  • 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.