41 KiB
| 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: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:
platform-core/modules/datamodels/datamodelDataSource.py - Änderung:
autoSync: bool = Field(default=False, ...)→ umbenennen zuragIndexEnabled: 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 zulastIndexed: Optional[float](gleicher Sinn, klarere Semantik)
- Begründung: F1 —
autoSyncundlastSyncedwerden 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
knowledgePreferencesaktualisieren — entferne Erwähnung von:neutralizeBeforeEmbed(kommt nachDataSource.neutralize)surfaceToggles(obsolet — pro DataSource viaragIndexEnabled)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,surfaceTogglesausloadConnectionPrefs-Output entfernen (DTO + Defaults). - Wirkung: Walker dürfen
prefs.neutralizeBeforeEmbednicht 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).
- Cache:
- Aktuell
- Public Re-Export:
serviceBackgroundJobs/__init__.py→cancelJobexportieren.
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 derenprovenance.dataSourceId == dataSourceId.listFileContentIndexByDataSource(dataSourceId: str) -> List[Dict]— für Inventar-Anzeige.
- Voraussetzung: Walker müssen
dataSourceIdinsprovenance-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
serviceBackgroundJobsgrün; neuer Testtest_cancelJob_*deckt: cancel running, cancel terminal (no-op), cancel unknown (False). (manuell verifiziert 2026-05-15) _makeProgressCallback-Cache verifiziert (kein DB-Read proisCancelled()-Call innerhalb 3s). (manuell verifiziert 2026-05-15)subConnectorPrefs.loadConnectionPrefsliefert keinneutralizeBeforeEmbedmehr. (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. 125–238):- 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"} - 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, )
- Vor jedem Authority-Branch: DataSources der Connection laden:
- Authority-Mapping (DataSource.sourceType → Walker):
Authority sourceType-Werte Walker msftsharepointFolder,onedriveFolderbootstrapSharepointmsftoutlookFolder,calendarFolder,contactFolderbootstrapOutlookgooglegoogleDriveFolderbootstrapGdrivegooglegmailFolderbootstrapGmailclickupclickupListbootstrapClickupinfomaniakkdriveFolderbootstrapKdrive(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
dataSourcesNoneoder leer → frühzeitig return mit{"skipped": True, "reason": "no_data_sources"}. - Sonst: Schleife über
dataSources. Für jededs:- Effektive
neutralize-Policy berechnen (siehe B.4). _walkFolder(folderPath=ds["path"], ...)aufrufen — statt heutigemadapter.browse("/").- Vor jedem File-Call und alle 50 Items:
if progressCb.isCancelled(): break.
- Effektive
- Wenn
- 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 —provenanceist 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 = Truezurück._finalizeResultpropagiert Feld. - Job-Result: Dispatcher hängt
cancelled: true, processedSoFar: Nan.
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
dataSourceIdin 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. 1ragIndexEnabledDataSource (sonst schon im Dispatcher abgefangen — Test verifiziert dass Job sauberskippedreturned). (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. 183–202) 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. 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:
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 ausdelete_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.bootstrapmit Payload-Erweiterung{"dataSourceIds": [sourceId]}— Dispatcher honoriert das Filter-Set (Phase B.1 Erweiterung). - Tabular (FeatureDataSource): Nicht in v1.
FeatureDataSourcewird überneutralize+neutralizeFieldsgesteuert, 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)→ ConnectionsgetRootInterface().db.getRecordset(DataSource, ...)→ DataSourcesinterfaceDbKnowledge.listFileContentIndexByConnection(connectionId)→ Chunk-CountsserviceBackgroundJobs.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-insightsan Z. 37–38; 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ältknowledgeIngestionEnabled+knowledgePreferencesin allen 3 Code-Pfaden. (manuell verifiziert 2026-05-15)PATCH /knowledge-consent enabled=falsepurged synchron + cancelt laufende Jobs (Test: Job wird CANCELLED innerhalb 5s). (manuell verifiziert 2026-05-15)PATCH /knowledge-consent enabled=trueenqueued Bootstrap nur wenn min. 1 RAG-DataSource existiert. (manuell verifiziert 2026-05-15)PATCH /datasources/{id}/rag-index trueenqueued Mini-Bootstrap mitdataSourceIds=[id]. (manuell verifiziert 2026-05-15)PATCH /datasources/{id}/rag-index falsepurged Chunks dieser DataSource (Verifikation vialistFileContentIndexByDataSource). (manuell verifiziert 2026-05-15)GET /api/rag/inventory/meliefert 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-statisticsist 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,mimeAllowlistentfernen.- 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)— wraptpatchKnowledgeConsent+refetch().setKnowledgePreferences(connectionId, prefs, opts)— wraptpatchKnowledgePreferences+refetch().stopKnowledgeJobs(connectionId)— wraptpostKnowledgeStop.
- Bestehend bleibt:
createInfomaniakConnection,submitInfomaniakTokenwerden vom Wizard intern aufgerufen (siehe D.3).
D.3 AddConnectionWizard.tsx Komplett-Refactor
- Datei:
ui-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:
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.handleAdminConsenthereinholen (Pattern: Popup-Window mit…/api/security/msft/admin-consentURL). - 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.
- Logik aus
- 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.
- Logik aus
- Neuer Connector hinzufügen:
ConnectorType = 'google' | 'msft' | 'clickup' | 'infomaniak'; Icon + Label entsprechend ergänzen. onConnect-Signatur: vereinfacht zu
(keinonConnect: (type: ConnectorType, knowledgeEnabled: boolean) => Promise<void>;prefsmehr — Standard-Defaults werden Backend-seitig bei OAuth-Callback gesetzt.)
D.4 ConnectionsPage.tsx aufräumen
- Datei:
ui-nyla/src/pages/basedata/ConnectionsPage.tsx - Entfernen:
adminConsentPendingState + Handler (Z. 57, alle Verwendungen).infomaniakModalState +handleCreateInfomaniak,handleInfomaniakCancel,handleInfomaniakSubmit(Z. 78–...).- Den Standalone-Button „Admin-Zustimmung" (Suche im File:
Admin-ZustimmungoderadminConsent). - 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.
- „Indexierung läuft (47%) · ✕ Stop" wenn
- „Einstellungen"-Link öffnet
KnowledgePreferencesDrawer(siehe D.5).
- Master-Toggle „Wissensdatenbank" (Switch-Style) — bound an
- Banner
syncBanner(Z. 60+): bleibt; wird aber durchrunningJobs-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 beiclickup. - 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:
- Type
UdbDataSource(Z. 36–45):ragIndexEnabled: booleanergänzen. - Konstante neu:
const _RAG_INDEX_ICON = '🧠'; // oder Material-Icon - Render-Erweiterung in jedem Tree-Renderer (
TreeNode,FeatureRecordRenderer,ParentGroupRenderer):- Neue Prop:
inheritedRagIndexEnabled?: booleananalog zuinheritedNeutralize. effectiveRagIndex = ds?.ragIndexEnabled ?? inheritedRagIndexEnabled ?? false.-
- Button rendern, parallele CSS zu
inheritedNeutralize(Opazität 0.35 wennfalse/inherited).
- Button rendern, parallele CSS zu
- Neue Prop:
- 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]); - Vererbungs-Visualisierung: identisch zur
inheritedNeutralize-Implementierung (gestrichelter Border, gedimmt).
- Type
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/meAlle 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/mandateMandantAdmin Aggregation pro User; Top-Contributors; Total-Chunks Plattform GET /api/rag/inventory/platformPlatformAdmin 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 5sgetRagJobsActive(); Statecount: number.- Render: nur wenn
count > 0— kleines Icon (FaSyncrotierend) mit Badgecount; Click → Navigate/system/rag-inventory.
- Einbindung: in
Header.tsxneben anderen Status-Badges.
D.10 Workspace-Insights-Seite löschen
- Löschen:
ui-nyla/src/pages/views/workspace/WorkspaceRagInsightsPage.tsxui-nyla/src/pages/views/workspace/WorkspaceRagInsightsPage.module.css
- Datei:
ui-nyla/src/pages/FeatureView.tsx - Entfernen: Import Z. 38, Mapping
'rag-insights': WorkspaceRagInsightsPageZ. 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-Sequenzconnector → consent → msftAdminConsent → connect. (manuell verifiziert 2026-05-15) - Wizard für
infomaniak: Step-Sequenzconnector → consent → infomaniakPat. PAT-Cancel löscht Connection. (manuell verifiziert 2026-05-15) - Wizard für
google/clickup: Step-Sequenzconnector → 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)RagInventoryPageerreichbar viaStart > 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)
WorkspaceRagInsightsPageist 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_skipsIfNoRagEnabledDataSourcestest_bootstrap_iteratesOnlyRagEnabledDataSourcestest_bootstrap_respectsCancelFlag(Walker stoppt innerhalb 50 Items)test_dataSourceToggleOff_purgesChunks(E2E PATCH → Chunk-Count = 0)test_consentOff_purgesAllConnectionChunks_andCancelsJobstest_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(oderdata-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/nach4-done/schieben:2026-05-rag-consent-and-control-unification.md2026-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)
- D-1: Renaming. Entschieden:
autoSync→ragIndexEnabled,lastSynced→lastIndexed. Umgesetzt viascript_db_migrate_datasource_rag.py. - D-2: Tabular RAG-Toggle: Nicht in v1.
FeatureDataSourcebleibt mitneutralize+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.