746 lines
41 KiB
Markdown
746 lines
41 KiB
Markdown
---
|
||
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<void>;
|
||
export async function patchKnowledgePreferences(connectionId: string, prefs: KnowledgePreferences, opts?: { triggerResync?: boolean }): Promise<void>;
|
||
export async function postKnowledgeStop(connectionId: string): Promise<{ cancelled: number; jobIds: string[] }>;
|
||
export async function patchDataSourceRagIndex(dataSourceId: string, enabled: boolean): Promise<void>;
|
||
export async function getRagInventoryMe(): Promise<RagInventoryDto>;
|
||
export async function getRagInventoryMandate(): Promise<RagInventoryDto>;
|
||
export async function getRagInventoryPlatform(): Promise<RagInventoryDto>;
|
||
export async function getRagJobsActive(): Promise<RagJobDto[]>;
|
||
```
|
||
|
||
### D.2 `useConnections` Hook erweitern
|
||
|
||
- **Datei:** `frontend_nyla/src/hooks/useConnections.ts`
|
||
- **Neu exportieren:**
|
||
- `setKnowledgeConsent(connectionId, enabled)` — wrapt `patchKnowledgeConsent` + `refetch()`.
|
||
- `setKnowledgePreferences(connectionId, prefs, opts)` — wrapt `patchKnowledgePreferences` + `refetch()`.
|
||
- `stopKnowledgeJobs(connectionId)` — wrapt `postKnowledgeStop`.
|
||
- **Bestehend bleibt:** `createInfomaniakConnection`, `submitInfomaniakToken` werden vom Wizard intern aufgerufen (siehe D.3).
|
||
|
||
### D.3 `AddConnectionWizard.tsx` Komplett-Refactor
|
||
|
||
- **Datei:** `frontend_nyla/src/components/AddConnectionWizard/AddConnectionWizard.tsx`
|
||
- **Entfernen:**
|
||
- `computeCostEstimate` (Z. 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<void>;
|
||
```
|
||
(kein `prefs` mehr — Standard-Defaults werden Backend-seitig bei OAuth-Callback gesetzt.)
|
||
|
||
### D.4 `ConnectionsPage.tsx` aufräumen
|
||
|
||
- **Datei:** `frontend_nyla/src/pages/basedata/ConnectionsPage.tsx`
|
||
- **Entfernen:**
|
||
- `adminConsentPending` State + Handler (Z. 57, alle Verwendungen).
|
||
- `infomaniakModal` State + `handleCreateInfomaniak`, `handleInfomaniakCancel`, `handleInfomaniakSubmit` (Z. 78–...).
|
||
- Den Standalone-Button „Admin-Zustimmung" (Suche im File: `Admin-Zustimmung` oder `adminConsent`).
|
||
- Den Standalone-Button „Infomaniak verbinden" (Suche: `Infomaniak`-Button-Render).
|
||
- **Neue UI pro Row** (in `FormGeneratorTable`-Custom-Renderer oder Side-Drawer):
|
||
- **Master-Toggle „Wissensdatenbank"** (Switch-Style) — bound an `connection.knowledgeIngestionEnabled`.
|
||
- Off-Confirm-Dialog: „X Inhalte werden aus dem RAG entfernt".
|
||
- **Status-Pille** (rechts neben Toggle):
|
||
- „Indexierung läuft (47%) · ✕ Stop" wenn `runningJobs.length > 0`.
|
||
- „Letzte Indexierung: vor 3 Stunden" sonst.
|
||
- **„Einstellungen"-Link** öffnet `KnowledgePreferencesDrawer` (siehe D.5).
|
||
- **Banner `syncBanner`** (Z. 60+): bleibt; wird aber durch `runningJobs`-Polling abgelöst (alle 5s).
|
||
|
||
### D.5 Neue Komponente `KnowledgePreferencesDrawer`
|
||
|
||
- **Datei:** `frontend_nyla/src/components/KnowledgePreferencesDrawer/KnowledgePreferencesDrawer.tsx` **(neu)**
|
||
- **Inhalt:** Felder analog zu altem Wizard-Step 2, aber **ohne** `neutralizeBeforeEmbed`:
|
||
- `mailContentDepth` (Select: metadata / snippet / full)
|
||
- `mailIndexAttachments` (Checkbox)
|
||
- `clickupScope` (Select: titles / title_description / with_comments)
|
||
- `clickupIndexAttachments` (Checkbox)
|
||
- `maxAgeDays` (Number)
|
||
- **Connector-aware Sichtbarkeit:** Mail-Felder nur bei `google`/`msft`; ClickUp-Felder nur bei `clickup`.
|
||
- **Speichern:** `setKnowledgePreferences(connectionId, prefs, { triggerResync: true })` — User wird gefragt ob Resync.
|
||
|
||
### D.6 `SourcesTab.tsx` — 4. Action-Button für `ragIndexEnabled`
|
||
|
||
- **Datei:** `frontend_nyla/src/components/UnifiedDataBar/SourcesTab.tsx`
|
||
- **Erweiterungen:**
|
||
1. Type `UdbDataSource` (Z. 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.*
|