From 111faae08881a39b48c25d390ff66e7cd83c6ea7 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Sun, 26 Apr 2026 23:59:12 +0200
Subject: [PATCH] added infomaniak
---
.../2-build/2026-04-infomaniak-connector.md | 121 ++++++++++++++++++
c-work/_CHANGELOG.md | 3 +
d-guides/infomaniak-oauth-setup.md | 105 +++++++++++++++
3 files changed, 229 insertions(+)
create mode 100644 c-work/2-build/2026-04-infomaniak-connector.md
create mode 100644 d-guides/infomaniak-oauth-setup.md
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`