diff --git a/c-work/2-build/2026-04-infomaniak-connector.md b/c-work/2-build/2026-04-infomaniak-connector.md new file mode 100644 index 0000000..bcdb91c --- /dev/null +++ b/c-work/2-build/2026-04-infomaniak-connector.md @@ -0,0 +1,121 @@ + + + + +# Infomaniak Connector (kDrive + Mail) + UDB-Integration + +## Beschreibung und Kontext + +PowerOn besitzt das Provider-Connector-Pattern fuer externe Datenanbindungen +(Microsoft, Google, ClickUp). Infomaniak war bisher nicht angebunden. Ziel: ein +neuer `InfomaniakConnector`, der OAuth gegen `login.infomaniak.com` durchfuehrt +und Daten aus zwei Services bereitstellt: + +- **kDrive** (Pendant zu OneDrive / Google Drive) +- **Mail** (Pendant zu Outlook / Gmail) + +Die Verbindung ist eine reine **Daten-Connection** (`TokenPurpose.DATA_CONNECTION`). +Infomaniak ist explizit **kein** Login-Provider fuer PowerOn -- entsprechend gibt +es nur `_FLOW_CONNECT`, kein `_FLOW_LOGIN`. + +Zusatzlich wurden zwei kleine Inkonsistenzen in der ClickUp-UDB-Anzeige +behoben (`_SERVICE_ICONS` + `_SERVICE_TO_SOURCE_TYPE`). + +## Fokus und kritische Details + +- **Refresh-Token-Persistenz**: Infomaniak rotiert Refresh-Tokens nicht in jedem + Token-Response; falls `refresh_token` im Callback fehlt, wird aus dem letzten + gespeicherten Token rekonstruiert (analog Google). +- **API-Pfad-Konvention**: Drei-Ebenen-Hierarchie kDrive (`/{driveId}/{fileId}`) + und vier-Ebenen Mail (`/{mailboxId}/{folderId}/{uid}`); `ServiceAdapter.browse` + unterscheidet die Tiefe via Segment-Count. +- **Antwort-Wrapping**: Erfolgreiche Responses sind als + `{result: 'success', data: ...}` gewrappt -- `_unwrapData()` normalisiert. +- **Authority-Filter** in `tokenRefreshService` muss `INFOMANIAK` enthalten, + sonst werden Token nie proaktiv refreshed. + +## Ziel und Nicht-Ziele + +- Ziel: Infomaniak-Daten (kDrive + Mail) wie MSFT/Google im UDB browsbar. +- Ziel: ConnectionsPage hat Button "Infomaniak" (analog ClickUp). +- Ziel: Refresh-Token-Lifecycle vollautomatisch (kein User-Reconnect noetig). +- Ziel: ClickUp-UDB-Inkonsistenz beheben (Service-Icon + Source-Type-Mapping). +- NICHT: Infomaniak als PowerOn-Login (`User`-Erstellung via Infomaniak-OAuth). +- NICHT: Upload-Implementierung in kDrive/Mail (gibt nur Browse + Download). + +## Betroffene Module + +- Gateway: + - `modules/datamodels/datamodelUam.py` (Enum) + - `modules/auth/oauthProviderConfig.py` (Scopes) + - `modules/auth/tokenManager.py` (Refresh) + - `modules/auth/tokenRefreshService.py` (Background-Refresh) + - `modules/connectors/providerInfomaniak/connectorInfomaniak.py` (neu) + - `modules/connectors/connectorResolver.py` (Registry) + - `modules/routes/routeSecurityInfomaniak.py` (neu) + - `modules/routes/routeDataConnections.py` (Dispatch) + - `app.py` (Router-Mount) +- Frontend: + - `src/api/connectionApi.ts` (Authority-Typ) + - `src/hooks/useConnections.ts` (Popup-Handler) + - `src/pages/basedata/ConnectionsPage.tsx` (Button) + - `src/components/UnifiedDataBar/SourcesTab.tsx` (Icons + Colors + Mapping, ClickUp-Fix) +- DB-Migration: nein (nur Enum-Wert, alle Tabellen abwaertskompatibel). +- Andere: keine. + +## Entscheidungen + +| Datum | Entscheidung | Begruendung | +|------------|--------------|-------------| +| 2026-04-26 | Self-contained Connector (httpx im Modul, kein eigener Service) | Folgt Google/MSFT-Pattern, nicht ClickUp-Pattern (das delegiert an `ClickupService`) | +| 2026-04-26 | Nur DATA_CONNECTION, kein Login | User explicitly: "wir benoetigen nur den userconnection auth" | +| 2026-04-26 | `kdrive` + `mail` als Service-Namen | Kurz, konsistent mit Infomaniak-Branding (kDrive heisst offiziell so) | + +## Umsetzungs-Checkliste + +- [x] AuthAuthority-Enum erweitert +- [x] Scopes definiert (`user_info kdrive mail`) +- [x] InfomaniakConnector + KdriveAdapter + MailAdapter +- [x] ConnectorResolver-Registry +- [x] OAuth-Route `/api/infomaniak/auth/connect[/callback]` +- [x] Token-Refresh (`refreshInfomaniakToken` + Background-Service) +- [x] DataConnections-Dispatch (`authority_map`, Labels, `connect_service`) +- [x] App-Router-Mount +- [x] Frontend-Typen +- [x] Hook `createInfomaniakConnectionAndAuth` + Event-Listener +- [x] ConnectionsPage-Button +- [x] UDB-Integration (Authority-Icon, Service-Icons, Source-Colors, Mapping) +- [x] ClickUp-UDB-Fix +- [x] Setup-Guide `wiki/d-guides/infomaniak-oauth-setup.md` +- [ ] Manueller End-to-End-Test (ClientID/Secret im Manager registrieren, Verbinden, kDrive browsen, Mail browsen, Datei downloaden) +- [ ] `wiki/b-reference/connectors.md` (falls vorhanden) ergaenzen + +## Akzeptanzkriterien + +| # | Kriterium (Given-When-Then) | Prio | +|---|-----------------------------|------| +| 1 | Given Infomaniak-Credentials in `APP_CONFIG`, When Nutzer "Infomaniak" auf ConnectionsPage klickt, Then oeffnet sich der OAuth-Popup auf `login.infomaniak.com/authorize` | must | +| 2 | Given erfolgreicher OAuth-Callback, Then ist die UserConnection `ACTIVE`, Token gespeichert (`tokenAccess`+`tokenRefresh`), `externalId/Email` gesetzt | must | +| 3 | Given aktive Connection, When im UDB die Authority expandiert wird, Then werden Services `kdrive` und `mail` mit Icons angezeigt | must | +| 4 | Given Token vor Ablauf < 5 min, When Background-Refresh laeuft, Then wird `_refresh_infomaniak_token` aufgerufen und Token erneuert | must | +| 5 | Given ClickUp-Service-Node im UDB, Then ist das Service-Icon das Klemmbrett (Fix) | should | + +## Testplan + +| ID | AC | Art | Automatisiert | Repo-Pfad | Status | +|----|----|-----|---------------|-----------|--------| +| T1 | 1-2 | manual | nein | UI: ConnectionsPage | pending | +| T2 | 3 | manual | nein | UI: UDB SourcesTab | pending | +| T3 | 4 | unit | spaeter | gateway/tests/auth/test_tokenManager.py | pending | +| T4 | 5 | manual | nein | UI: UDB SourcesTab | pending | + +## Links + +- PR: tbd +- Setup-Guide: `wiki/d-guides/infomaniak-oauth-setup.md` + +## Abschluss + +- [ ] `b-reference/` aktualisiert +- [ ] `TOPICS.md` aktualisiert +- [ ] Dokument nach `z-archive/` verschoben diff --git a/c-work/_CHANGELOG.md b/c-work/_CHANGELOG.md index e5bafad..489be11 100644 --- a/c-work/_CHANGELOG.md +++ b/c-work/_CHANGELOG.md @@ -14,6 +14,9 @@ Skip: reine Refactors, Formatting, Lint, Dep-Bumps, Test-only, Wiki-Tippfehler. ## 2026-04-26 +- 2026-04-26 | fix | gateway | Feature-Data-Subagent (`queryFeatureInstance`) Schema-Prompt war zu duenn: Agent erhielt nur Tabellennamen + flache Spalten-Liste, ohne Typen / Beschreibungen / FK-Beziehungen. Folge: bei Trustee-Saldo-Queries summierte er `TrusteeDataJournalLine.debitAmount/creditAmount` ohne Datumsfilter (JournalLine hat gar kein `bookingDate`!) statt die Periode-bezogenen `TrusteeDataAccountBalance.closingBalance` zu nutzen; ISO-Date-Strings wurden gegen Float-Unix-Timestamps gefiltert (`bookingDate <= '2025-12-31'`). Fix in `featureDataAgent._buildSchemaContext`: pro Tabelle wird jetzt der zugehoerige Pydantic-Klasse via `MODEL_REGISTRY` resolved und pro selektiertem Feld eine angereicherte Zeile gerendert -- Python-Typ aus `field.annotation`, deutsches Label aus `json_schema_extra.label`, Description aus `Field(description=...)`, FK-Target aus `fk_target`. Dazu drei neue Regeln im System-Prompt: (a) Float-Felder mit "unix timestamp" in der Description sind Sekunden-seit-Epoch (Beispiel `'2025-12-31' -> 1735603200.0`), (b) Tools koennen nicht JOINen -- FK-Tabellen separat abfragen, (c) Periode-aggregierte Tabellen (Opening/Closing-Balances) bevorzugt vor SUM ueber Rohdaten. Fallback auf flache Feldliste wenn die Tabelle nicht im Pydantic-Registry ist. Unit-Tests in `tests/unit/services/test_featureDataAgent_schema.py` +- 2026-04-26 | feat | gateway+frontend-nyla | Infomaniak-Connector (kDrive + Mail) als neuer ProviderConnector analog Google/MSFT/ClickUp. Backend: `AuthAuthority.INFOMANIAK`, `providerInfomaniak/connectorInfomaniak.py` mit `KdriveAdapter`+`MailAdapter` (httpx, OAuth-Bearer, Path-Konvention `/{driveId}/{fileId}` bzw. `/{mailboxId}/{folderId}/{uid}`), `routeSecurityInfomaniak.py` mit `_FLOW_CONNECT`-only (kein Login -- pure DATA_CONNECTION), Token-Refresh via `tokenManager.refreshInfomaniakToken` + `tokenRefreshService._refresh_infomaniak_token`, `connectorResolver`-Registry-Eintrag, Authority-Map/Labels/Dispatch in `routeDataConnections`, Router-Mount in `app.py`. Refresh-Token-Persistierung holt bei fehlendem `refresh_token`-Response aus dem letzten gespeicherten Token (analog Google). Frontend: `connectionApi.ts` Authority-Typ erweitert, `useConnections.createInfomaniakConnectionAndAuth` + `infomaniak_connection_success/error`-Event-Listener, `ConnectionsPage` mit Infomaniak-Button (FaCloud), `SourcesTab` Icons/Colors/Service-Mapping fuer `infomaniak`/`kdrive`/`mail` -- inkl. Fix der bisher fehlenden ClickUp-Eintraege in `_SERVICE_ICONS` + `_SERVICE_TO_SOURCE_TYPE`. Setup-Guide unter `wiki/d-guides/infomaniak-oauth-setup.md` (c-work: c-work/2-build/2026-04-infomaniak-connector.md) +- 2026-04-26 | fix | gateway | Feature-Data-Subagent (`queryFeatureInstance`) hat seine Loop hardcoded auf 8 Runden begrenzt, unabhaengig vom Workspace `maxAgentRounds`. Im int-System brachen Trustee-Saldo-Queries deshalb mit `Maximum rounds reached. Progress after 8 rounds` ab, obwohl der Parent-Agent z.B. mit 25 Runden konfiguriert war. Fix: `agentLoop._executeToolCalls` propagiert jetzt `parentMaxRounds` + `parentMaxCostCHF` ueber den Tool-Context; `_featureSubAgentTools._queryFeatureInstance` liest sie aus und reicht sie an `runFeatureDataAgent(maxRounds=, maxCostCHF=)` weiter. Default fuer Direktaufrufer/Tests bleibt 8. Cost-Cap skaliert linear (`_MAX_COST_CHF_PER_ROUND = 0.02 * maxRounds`), damit nicht der 0.15-CHF-Guard die Loop vor Erreichen der konfigurierten Runden abschiesst (25 Runden -> 0.50 CHF). Subagent-Start loggt jetzt effektive `maxRounds`/`maxCostCHF` zur Diagnose - 2026-04-26 | feat | gateway+frontend-nyla | Database-Health Orphan-Cleanup: neue Checkbox `Ohne FK-Referenzen zu UserInDB.id` (default ON). Deleted-User-Reste in Audit/Billing/Membership-Tabellen sammeln sich natuerlich an wenn ein User geloescht wird und gehoeren in den separaten User-Purge-Workflow, nicht in die generische FK-Bereinigung. Backend: `_isUserIdFk(targetTable, targetColumn)`-Helper (case-insensitive auf Tabellenname); `_cleanAllOrphans(force, excludeUserFks)` ueberspringt entsprechende Relationen; `/orphans?excludeUserFks=true` filtert Scan-Resultate; `OrphanCleanAllRequest.excludeUserFks` filtert clean-all. Frontend: Checkbox neben `Nur Probleme`, default checked, mit Tooltip; URL-Param + clean-all Body-Field synchron; `Alle bereinigen`-Counter zeigt jetzt nur Non-User-FK-Orphans - 2026-04-26 | fix | gateway | aicorePluginOpenai: `max_tokens` durch `max_completion_tokens` ersetzt in `callAiBasic` und `callAiBasicStream`. Hintergrund: OpenAI lehnt `max_tokens` fuer gpt-5.x / o-series Modelle mit HTTP 400 `unsupported_parameter` ab (`Use 'max_completion_tokens' instead`). Im Log `log_app_20260426.log` (L741-764) sichtbar: `gpt-5.4-nano` failover scheiterte sofort, ModelSelector wechselte auf `claude-opus-4-6`. Per OpenAI API-Reference akzeptieren ALLE aktuellen Chat-Completions-Modelle (legacy gpt-4o/gpt-4.1, gpt-5.x, o1/o3/o4) `max_completion_tokens`, daher universeller Wechsel statt Modell-spezifischer Verzweigung - 2026-04-26 | feat | gateway | PDF-Renderer Emoji-Support: Noto Emoji (monochrome, OFL) als Fallback-Font registriert. Bisher rendern WinAnsi-Core-Fonts (Helvetica/Courier) Emoji-Codepoints (U+2600+, U+1F300+) als fehlende Glyphen-Quadrate. Neu unter `gateway/assets/fonts/NotoEmoji-Regular.ttf` (~419 KB, 887 Codepoints) + `_pdfFontFallback.py` Helper: registriert die TTF einmalig bei reportlab, scannt deren cmap, und `wrapEmojiSpansInXml` umschliesst zusammenhaengende Emoji-Runs (codepoint >= U+2000 ∧ in cmap) mit `` — nestet sauber in ``/``/``. `rendererPdf._markdownInlineToReportlabXml` wendet das am Ende an, also greift es ueberall wo Paragraph-Markup gebaut wird (Headings, Paragraphs, Bullet-Lists, Table-Cells, extracted_text). `Preformatted` (Code-Blocks) ist Single-Font-only und bleibt unveraendert — Emojis in Code-Bloecken sind selten, Box-Drawing wird wie bisher zu ASCII normalisiert. Smoke-Test in `test_renderer_pdf_smoke.py` diff --git a/d-guides/infomaniak-oauth-setup.md b/d-guides/infomaniak-oauth-setup.md new file mode 100644 index 0000000..d195d44 --- /dev/null +++ b/d-guides/infomaniak-oauth-setup.md @@ -0,0 +1,105 @@ +# Infomaniak OAuth 2.0 Setup Guide + +## Overview + +This guide explains how to register an Infomaniak OAuth application so that PowerOn +users can connect their Infomaniak account as a data source. The connection +exposes two services in the Unified Data Bar: + +- **kDrive** -- browse and download files from any drive accessible to the user. +- **Mail** -- browse mailboxes, folders, and download messages as `.eml`. + +Infomaniak is a **data-only** authority in PowerOn. It is **not** a login +provider; users still authenticate against PowerOn via Local / Google / Microsoft. + +## Prerequisites + +- An Infomaniak account with admin rights to create API credentials. +- Access to the Infomaniak Manager (https://manager.infomaniak.com). +- The PowerOn gateway publicly reachable (or `http://localhost:8000` for development). + +## Step 1: Create an Infomaniak API Application + +1. Login to https://manager.infomaniak.com. +2. Navigate to your account icon (top right) > **API**. +3. Click **Create a new application**. +4. Fill in: + - **Name:** PowerOn (or your project name) + - **Description:** Data connector for kDrive + Mail + - **Redirect URI:** must exactly match the gateway endpoint: + - Development: `http://localhost:8000/api/infomaniak/auth/connect/callback` + - Production: `https:///api/infomaniak/auth/connect/callback` +5. Select scopes: + - `user_info` -- required to read `/1/profile` for `externalId` / `email`. + - `kdrive` -- required for the kDrive ServiceAdapter. + - `mail` -- required for the Mail ServiceAdapter. +6. Save and copy the generated **Client ID** and **Client Secret**. + +## Step 2: Configure PowerOn Gateway + +Add the following keys to your gateway environment file (e.g. `gateway/env_dev.env`): + +```env +# Infomaniak OAuth -- Data App (kDrive + Mail) +Service_INFOMANIAK_DATA_CLIENT_ID = your-client-id +Service_INFOMANIAK_DATA_CLIENT_SECRET = your-client-secret +Service_INFOMANIAK_OAUTH_REDIRECT_URI = http://localhost:8000/api/infomaniak/auth/connect/callback +``` + +For production, replace the redirect URI with your HTTPS gateway host. The URI +must be identical (byte-for-byte) to the one registered in step 1. + +Restart the gateway after editing the env file so `APP_CONFIG` is reloaded. + +## Step 3: Verify the Flow + +1. Login to PowerOn and open **Basisdaten > Verbindungen**. +2. Click **Infomaniak** in the header actions. +3. A popup opens against `https://login.infomaniak.com/authorize`. +4. Sign in with your Infomaniak account, grant the requested scopes. +5. The popup closes automatically and the new connection appears with status + `connected`. +6. Open the **Unified Data Bar > Sources** tab; you should see the Infomaniak + authority node with `kdrive` and `mail` services. + +## Token Lifecycle + +- Access tokens are short-lived (refresh handled automatically by + `tokenRefreshService._refresh_infomaniak_token`). +- Refresh tokens are stored alongside the connection (`Token.tokenRefresh`). +- If a refresh returns `invalid_grant`, the user must reconnect (popup again + via the Infomaniak button on `ConnectionsPage`). + +## Troubleshooting + +### `invalid_redirect_uri` +The URI registered on Infomaniak Manager does not match `Service_INFOMANIAK_OAUTH_REDIRECT_URI`. +They must be byte-identical (including protocol, host, port, and path). + +### `invalid_client` +Client ID or secret in the env file does not match what Infomaniak issued. +Re-copy the values from Manager > API. + +### Profile lookup returns 401 +The `user_info` scope was not granted. Re-create the application with the +correct scope set, or reconnect the user. + +### Empty kDrive listing +The user has no kDrives associated. Verify on https://kdrive.infomaniak.com +that at least one drive is accessible. + +## Security Notes + +- Never commit the client secret. Use the encrypted env mechanism described in + `wiki/d-guides/encrypt-env-secrets.md`. +- Rotate the client secret if it leaks; update `Service_INFOMANIAK_DATA_CLIENT_SECRET` + and restart the gateway. Existing tokens remain valid only until they expire. +- The connector is scoped to `DATA_CONNECTION` only. Even with valid Infomaniak + credentials, no PowerOn login session is created. + +## Reference + +- Code: `gateway/modules/connectors/providerInfomaniak/connectorInfomaniak.py` +- OAuth route: `gateway/modules/routes/routeSecurityInfomaniak.py` +- Token refresh: `gateway/modules/auth/tokenManager.py::refreshInfomaniakToken` +- Frontend hook: `frontend_nyla/src/hooks/useConnections.ts::createInfomaniakConnectionAndAuth`