wiki/c-work/4-done/2026-04-infomaniak-connector.md
ValueOn AG 6eeeb962f6 upd
2026-04-29 20:23:03 +02:00

303 lines
21 KiB
Markdown

<!-- status: build -->
<!-- started: 2026-04-26 -->
<!-- pivoted: 2026-04-28 (OAuth -> Personal Access Token) -->
<!-- pivoted: 2026-04-28 (Mail-Endpoint nicht PAT-faehig -> Calendar als zweiter aktiver Service) -->
<!-- pivoted: 2026-04-28 (Contacts-PIM-Endpoint funktioniert -> Contacts als dritter aktiver Service) -->
<!-- pivoted: 2026-04-29 (kDrive-Listing fuer non-admin-User leer -> `/2/drive/init?with=drives` als Discovery) -->
<!-- component: gateway, frontend-nyla -->
# Infomaniak Connector (kDrive + Calendar + Contacts today; Mail reserved) + 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 Daten aus den Infomaniak-Services
bereitstellt. Heute aktiv:
- **kDrive** (Pendant zu OneDrive / Google Drive)
- **Calendar** (Pendant zu Outlook-Kalender / Google Calendar; .ics-Download)
- **Contacts** (Pendant zu Outlook-Kontakte / Google Contacts; .vcf-Download)
Blockiert (Scope ist auf der PAT, Adapter wartet auf Vendor):
- **Mail** -- alle erschoepfend getesteten Pfade scheitern. Final-Befund
vom 2026-04-28:
| Pfad | Status |
|---|---|
| `api.infomaniak.com/1/mail` | 404 nginx (existiert nicht) |
| `api.infomaniak.com/2/mail?account_id=...` | 404 nginx (existiert nicht) |
| `mail.infomaniak.com/api/mail?account_id=...` | 302 -> `login.infomaniak.com/authorize` (OAuth Web-Session, nicht PAT) |
| `mail.infomaniak.com/api/mail/?account_id=...` | 301 -> `http://mail.infomaniak.com:5000` (interner Cyrus-IMAP-API-Port, von aussen nicht erreichbar) |
| `mail.infomaniak.com/api/pim/mail` | 302 -> OAuth |
| `mail.infomaniak.com/api/pim/mailbox` | 302 -> OAuth |
| `mail.infomaniak.com/api/pim/folder` | 302 -> OAuth |
Konsequenz: PowerOn kann mit dem heutigen PAT-Mechanismus die
Infomaniak-Mailbox **nicht** ansprechen. Der Scope `workspace:mail`
bleibt im Standard-PAT-Setup, sodass der MailAdapter ohne
Token-Rotation aufgeschaltet werden kann, sobald Infomaniak einen
PAT-tauglichen Endpoint freischaltet (oder wir entscheiden, die
Mail-Connection ueber einen separaten OAuth-Flow oder ein App-Passwort
ueber IMAP/SMTP einzubinden -- separater Build).
Die Verbindung ist eine reine **Daten-Connection** (`TokenPurpose.DATA_CONNECTION`).
Infomaniak ist explizit **kein** Login-Provider fuer PowerOn.
Zusaetzlich wurden zwei kleine Inkonsistenzen in der ClickUp-UDB-Anzeige
behoben (`_SERVICE_ICONS` + `_SERVICE_TO_SOURCE_TYPE`).
## Architektur-Pivot 2026-04-28: OAuth -> Personal Access Token
**Befund**: Infomaniaks `login.infomaniak.com/authorize` akzeptiert nur
Identity-Scopes (`openid`, `profile`, `email`, `phone`). Bearer-Tokens, die
gegen die Daten-APIs (`/2/drive/...`, `/1/mail/...`) funktionieren, koennen
ueber OAuth nicht ausgestellt werden -- der Versuch quittiert mit
`error=invalid_scope`. Datenzugriff laeuft bei Infomaniak ausschliesslich
ueber **Personal Access Tokens (PATs)**, die der Endnutzer manuell im
Infomaniak-Manager erstellt.
**Konsequenz**: kompletter Umbau des Auth-Teils des Connectors. Das
Provider-Connector-Pattern und die Adapter (`KdriveAdapter`, `MailAdapter`)
bleiben unveraendert -- sie konsumieren ohnehin nur einen Bearer-Token. Was
aussortiert wurde:
- OAuth-Routen (`/api/infomaniak/auth/connect[/callback]`) -> entfernt.
- Token-Refresh-Logik (`refreshInfomaniakToken`, Background-Refresh-Branch)
-> entfernt; PATs sind langlebig, kein Rotations-Lifecycle.
- Scope-Definition `infomaniakDataScopes` -> entfernt.
- `Service_INFOMANIAK_*` Env-Variablen (Client-ID/Secret/Redirect-URI)
-> entfernt; PowerOn benoetigt keine Infomaniak-App-Registrierung mehr.
- Frontend-OAuth-Popup -> ersetzt durch Modal mit Token-Eingabe + Deeplink
zum Infomaniak-Manager.
**Neuer Auth-Flow**:
1. FE: User klickt "Infomaniak" -> `POST /api/connections/` mit
`authority=infomaniak` legt PENDING-Connection an.
2. FE: Modal oeffnet sich mit Schritt-fuer-Schritt-Anleitung + Link
`https://manager.infomaniak.com/v3/ng/accounts/token/list` und
Token-Eingabefeld.
3. FE: User fuegt PAT ein -> `POST /api/infomaniak/connections/{id}/token`.
4. BE: Validiert PAT in zwei Schritten:
- `resolveOwnerIdentity()` ruft PIM Calendar (Scope
`workspace:calendar`) auf, faellt bei leerer Owner-Liste auf PIM
Contacts zurueck (Scope `workspace:contact`). Beide Endpoints
liefern den ersten Owner-Record als `{account_id, name, user_id}`.
Wenn keiner antwortet -> 400 fail-loud (PAT ist fuer kDrive
unbrauchbar, weil `/2/drive` `account_id` zwingend braucht und kein
Drive-Endpoint die Identity ohne `user_info`-Scope rausgibt).
- `GET /2/drive?account_id={resolved}` -- erwartet 200, andernfalls
400 (`drive`-Scope fehlt) bzw. 502 (Infomaniak antwortet
unerwartet). Mit der vorab resolvten `account_id` ist der Probe
deterministisch und braucht keine 422-Tolerance mehr.
Token landet als `Token` mit 10-Jahres-Horizont (`tokenStatus`-Anzeige,
analog ClickUp). `externalUsername` und `externalId` werden fuer die
UI-Anzeige gesetzt; **`externalId` ist nicht Vertragspartner des
kDrive-Adapters** (siehe "Kritische Details").
5. FE: bei Erfolg -> Modal schliesst, Liste refresht; bei Fehler ->
Connection bleibt PENDING, Modal zeigt Fehlermeldung; bei Cancel ->
Connection wird via DELETE wieder entfernt.
## Fokus und kritische Details
- **API-Pfad-Konvention** (Adapter-Path != API-Path):
- kDrive (`api.infomaniak.com`): Adapter-Path `/{driveId}/{fileId}`,
API `/2/drive/{driveId}/files/{fileId}`. **Wichtig**: das
naheliegende `/2/drive?account_id=...`-Listing ist filtered auf
Drive-Manager-Admins (`account_admin: true`) und liefert fuer
normale kSuite-Member (`role: 'user'`) sauber `200 []`, obwohl
sie das Drive lesen koennen. Der korrekte User-zentrische
Endpoint ist `GET /2/drive/init?with=drives`, der ALLE Drives
des PAT-Owners zurueckgibt -- inklusive `id`, `name`, `account_id`,
`role`. Der Adapter cached die Liste auf der Instanz
(`_ensureDrives`); ein Browse zahlt also pro Request maximal einen
`init`-Call. `/2/drive/init?with=drives` braucht NUR den `drive`-
Scope, kein `accounts` oder `user_info`.
- Calendar (`calendar.infomaniak.com/api/pim`): Adapter-Path
`/{calendarId}/{eventId}`. **Achtung**: die naheliegenden
Nested-Routes (`/calendar/{id}/event`, `/calendar/{id}/event/{id}`,
`/calendar/{id}/event/{id}/export`) sind **NICHT** PAT-faehig --
sie 302-en zur OAuth-Login-Seite. Korrekte PAT-Pfade:
- Listing: `/api/pim/event?calendar_id={id}&from=YYYY-MM-DD HH:MM:SS&to=...`
mit Pflicht-Range, max **3 Monate** (Vendor-Constraint).
Adapter wahlt fix 90-Tage-Window (heute -30 / +60).
- Detail: `/api/pim/event/{eventId}` (ohne calendar-Praefix)
- Export `.ics`: `/api/pim/event/{eventId}/export`
- Contacts (`contacts.infomaniak.com/api/pim`): Adapter-Path
`/{addressBookId}/{contactId}`. Listing der AddressBooks und der
Contacts geht via PAT, **aber Detail- und Export-Endpoints
(`/addressbook/{book}/contact/{id}` und `.../export`) sind nicht
PAT-faehig** (500 bzw. 302 OAuth). Konsequenzen:
- Listing braucht zwingend
`&with=emails,phones,addresses,details`, sonst kommen die
relevanten Felder leer (Default-Response listet nur Stammdaten).
- `.vcf`-Download wird im Adapter **selbst synthetisiert** aus dem
Listing-Record (vCard 3.0). Implementierung in
`_renderInfomaniakVcard()`.
- Geteilte Organisations-Adressbuecher haben einen leeren Namen --
der Adapter setzt dort einen Platzhalter "Organisation".
`ServiceAdapter.browse` unterscheidet die Tiefe via Segment-Count.
- **Multi-Host**: `_infomaniakGet` / `_infomaniakDownload` akzeptieren ein
optionales `baseUrl`, sodass Calendar gegen `calendar.infomaniak.com`,
Contacts gegen `contacts.infomaniak.com` und kDrive gegen
`api.infomaniak.com` laufen.
- **Zwei Resolver, getrennte Verantwortung**:
- `resolveOwnerIdentity(token)` -> Display-Name + kSuite-`account_id`,
rein fuer das UI-Label der Connection. Erste Quelle PIM Calendar,
Fallback PIM Contacts; nimmt den ersten Owner-Record (`user_id > 0`
und `isinstance(account_id, int)`).
- `listAccessibleDrives(token)` -> Liste **aller** Drives, die der
PAT-Owner sehen kann (egal mit welcher Rolle). Ein einzelner
`GET /2/drive/init?with=drives`-Call, vom `KdriveAdapter` benutzt
statt des admin-only `/2/drive?account_id=...` Listings.
Beide raisen `InfomaniakIdentityError` mit einer scope-spezifischen
Fehlermeldung, sodass der Submit-Endpoint pro fehlendem Scope eine
eigene 400-Message zurueckgeben kann.
- **`externalId` ist UI-State, kein Adapter-Vertragspartner**: Submit
speichert `externalId = str(kSuiteAccountId)` fuer die ConnectionsPage
(Anzeige + Konsistenz mit anderen Providern), aber der KdriveAdapter
liest ihn nie -- er fragt zur Laufzeit `/2/drive/init?with=drives`.
- **Antwort-Wrapping**: Erfolgreiche Responses sind als
`{result: 'success', data: ...}` gewrappt -- `_unwrapData()` normalisiert.
- **Token-Validation** im Submit-Endpoint: zwei harte 200-Schritte,
jeder probet genau einen Scope:
1. `listAccessibleDrives` -> probet `drive`-Scope und stellt sicher,
dass mindestens ein erreichbarer kDrive existiert.
2. `resolveOwnerIdentity` -> probet `workspace:calendar` /
`workspace:contact`-Scope (mindestens einer noetig).
Jeder Schritt liefert eine eigene 400-Message, die exakt den fehlenden
Scope nennt.
- **Token-Persistenz**: `expiresAt = now + 10*365*24*3600`, `tokenRefresh = None`;
`getTokenStatusForConnection` zeigt damit `active`, kein "none".
## Ziel und Nicht-Ziele
- Ziel: Infomaniak-Daten (kDrive + Mail) wie MSFT/Google im UDB browsbar.
- Ziel: ConnectionsPage hat Button "Infomaniak" mit PAT-Modal.
- Ziel: Setup ohne Operator-Interaktion (keine globale App-Registrierung).
- Ziel: ClickUp-UDB-Inkonsistenz beheben (Service-Icon + Source-Type-Mapping).
- NICHT: Infomaniak als PowerOn-Login.
- NICHT: Upload-Implementierung in kDrive/Mail.
- NICHT: Auto-Refresh fuer Infomaniak (PATs sind langlebig; bei Ablauf legt
der User eine neue Connection an).
## Betroffene Module
- Gateway:
- `modules/datamodels/datamodelUam.py` (Enum erweitert)
- `modules/auth/oauthProviderConfig.py` (Infomaniak-Scopes raus)
- `modules/auth/tokenManager.py` (Infomaniak-Init + Refresh raus)
- `modules/auth/tokenRefreshService.py` (Infomaniak aus Background-Refresh raus)
- `modules/connectors/providerInfomaniak/connectorInfomaniak.py` (Adapter unveraendert)
- `modules/connectors/connectorResolver.py` (Registry, unveraendert)
- `modules/routes/routeSecurityInfomaniak.py` (komplett neu: PAT-Submit-Endpoint)
- `modules/routes/routeDataConnections.py` (`connect_service` antwortet bei
Infomaniak mit 400 + Hinweis)
- `gateway/.env` + `env_dev/int/prod[_forgejo].env` (Service_INFOMANIAK_* raus)
- Frontend:
- `src/api/connectionApi.ts` (`submitInfomaniakToken` neu)
- `src/hooks/useConnections.ts` (`createInfomaniakConnection` +
`submitInfomaniakToken` ersetzen `createInfomaniakConnectionAndAuth`,
OAuth-Popup-Branch fuer Infomaniak entfernt)
- `src/pages/basedata/ConnectionsPage.tsx` (PAT-Modal)
- `src/components/UnifiedDataBar/SourcesTab.tsx` (unveraendert seit Phase 1)
- Doku:
- `wiki/d-guides/infomaniak-oauth-setup.md` -> geloescht
- `wiki/d-guides/infomaniak-token-setup.md` -> neu
- DB-Migration: nein.
## Entscheidungen
| Datum | Entscheidung | Begruendung |
|------------|--------------|-------------|
| 2026-04-26 | Self-contained Connector (httpx im Modul, kein eigener Service) | Folgt Google/MSFT-Pattern, nicht ClickUp-Pattern |
| 2026-04-26 | Nur DATA_CONNECTION, kein Login | User explicitly: "wir benoetigen nur den userconnection auth" |
| 2026-04-26 | `kdrive` + `mail` als Service-Namen | Konsistent mit Infomaniak-Branding |
| 2026-04-28 | OAuth raus, Personal Access Token rein | Infomaniak's `/authorize` unterstuetzt fuer Datenzugriff keine Scopes; nur PATs liefern brauchbare Bearer-Tokens (`error=invalid_scope` bei OAuth-Versuch) |
| 2026-04-28 | Token-Validierung gegen `/1/profile` | Leichter Endpoint, liefert User-Identity (`id`, `login`, `email`) zur Anzeige im FE; gibt 401 bei ungueltigem PAT |
| 2026-04-28 | `/1/profile` raus, Validierung gegen `/2/drive` + `/1/mail` | Profile braucht zusaetzlich Scope `user_info`; mit Drive- + Mail-Probes verifizieren wir nur die fuer Adapter benoetigten Scopes |
| 2026-04-28 | `/1/mail` raus, Validierung gegen `calendar.infomaniak.com` + `/2/drive` | `/1/mail` existiert nicht (404 nginx); `mail.infomaniak.com/api/mail` redirected zu OAuth (302). Calendar-PIM-Endpoint funktioniert mit PAT und liefert Identity (account_id/user_id/name) gleich mit. |
| 2026-04-28 | `account_id` in `UserConnection.externalId` persistieren (zurueckgenommen 2026-04-28 abends) | War als Cache-Optimierung gedacht, hat aber zwei Concerns vermischt: (1) UI-Anzeige der Connection und (2) Adapter-Vertragspartner. Existierende Connections konnten dadurch mit einem Token-Fingerprint statt account_id korrumpiert werden, was im kDrive-Browse zu 422 fuehrte. |
| 2026-04-28 | `KdriveAdapter` resolvt `account_id` zur Laufzeit selbst (`_ensureAccountId`) ueber `resolveOwnerIdentity()` | Saubere Trennung: `externalId` ist nur UI-State, der Adapter ist self-contained und braucht keine Connection-Felder zu lesen. Damit gibt es keinen Migrationspfad und keine Korruption mehr -- der Adapter heilt sich automatisch. Cache auf Adapter-Instanz vermeidet wiederholte API-Calls innerhalb desselben Requests. |
| 2026-04-28 | Identity-Resolver (Calendar -> Contacts) zentral in `resolveOwnerIdentity()` | Beide PIM-Endpoints liefern dieselbe Owner-Struktur (`user_id`/`account_id`/`name`); ein gemeinsamer Resolver verhindert, dass Submit und Adapter divergieren. Die Sequenz Calendar -> Contacts deckt alle realistischen Setups ab (mindestens einer der beiden Scopes muss auf der PAT sein, damit kDrive ueberhaupt nutzbar ist). |
| 2026-04-29 | Calendar-Events ueber `/api/pim/event?calendar_id=...` (nicht `/calendar/{id}/event`) | Live-Test 2026-04-29: nested Route 302 zu OAuth, flat Route 200. Die offizielle PAT-faehige Route fordert `from`/`to` als Y-m-d H:i:s mit max-3-Monats-Range -- Adapter waehlt fixes 90-Tage-Window. |
| 2026-04-29 | `.vcf`-Download per Hand synthetisieren (`_renderInfomaniakVcard`) | Alle Contacts-Detail-/Export-Endpoints sind nicht PAT-faehig (500 bzw. 302 OAuth). Wir holen das Listing mit `with=emails,phones,addresses,details` (PAT-faehig) und rendern vCard 3.0 selbst -- konsistent mit MSFT/Google-Contacts-Adapter, der dieselbe Synthese betreibt. |
| 2026-04-29 | `KdriveAdapter` discoverte Drives ueber `/2/drive/init?with=drives` statt `/2/drive?account_id=X` | Live-Beweis vom User: Drive 2980592 existiert in account 1696919 mit Files (PDF + 2 Folders), `/2/drive?account_id=1696919` antwortet trotzdem `200 []`, `/2/drive/2980592/files` zeigt alle Files. Diagnose: `/2/drive` ist Drive-Manager-Admin-Sicht (filtered auf `account_admin: true`), normaler kSuite-Member (`role: 'user'`) sieht dort nichts -- ist nicht Vendor-Bug, sondern Spec. Loesung gefunden via Endpoint-Probing: `/2/drive/init?with=drives` ist die User-zentrische Drive-Liste, die ALLE zugaenglichen Drives unabhaengig von der Admin-Rolle liefert (verifiziert mit PAT der `accounts`-Scope explizit nicht hat -> 200, alle Drives drin). Damit wird der zwischenzeitlich eingefuehrte `accounts`-Scope-Workaround wieder entfernt -- er war eine Sackgasse, weil `/1/accounts` die Manager-Organizations listet (1 pro User in den meisten Faellen), nicht die Drive-Accounts. Der `init`-Endpoint braucht nur den `drive`-Scope, der bereits im PAT-Standard-Setup ist; keine Token-Rotation noetig. |
| 2026-04-28 | MailAdapter und ContactAdapter NICHT registrieren bis Endpoint gefunden | 302-Redirects zu OAuth wuerden im UDB als kaputter Service erscheinen; lieber gar nicht zeigen. |
| 2026-04-28 | ContactAdapter aktivieren via `contacts.infomaniak.com/api/pim/addressbook` | Nachgereichter curl-Test zeigte 200 + JSON mit derselben Struktur wie Calendar (`addressbooks[].id/name/account_id/...`). Singular-Pfad funktioniert (`/contact` und `/contacts` reden weiter mit OAuth). Adapter ist 1:1 analog `CalendarAdapter`, nur mit `addressbook` statt `calendar` und `.vcf` statt `.ics`. |
| 2026-04-28 | `expiresAt = now + 10y` fuer PATs | Analog ClickUp, sonst markiert `getTokenStatusForConnection` die Connection als "none" |
| 2026-04-28 | Connection up-front, dann PAT-Submit (statt Submit-erst-dann-erstellen) | Nutzt vorhandenen `POST /api/connections/`-Pfad ohne Sonderbehandlung; Cancel rollback via DELETE auf Modal-Schliessen |
## Umsetzungs-Checkliste
- [x] AuthAuthority-Enum erweitert
- [x] InfomaniakConnector + KdriveAdapter (discovert Drives zur
Laufzeit ueber `listAccessibleDrives()` -> `/2/drive/init?with=drives`,
cached die Liste auf der Adapter-Instanz) + CalendarAdapter
+ ContactAdapter
- [x] `resolveOwnerIdentity()` als reiner UI-Identity-Helper
(Display-Name + kSuite-account_id fuer das Connection-Label)
- [x] `listAccessibleDrives()` als Drive-Discovery-Helper
(`/2/drive/init?with=drives`, `drive`-Scope-Probe, raised
`InfomaniakIdentityError` mit klarer Scope-Message)
- [x] ConnectorResolver-Registry (alle Adapter werden uniform mit
`accessToken` konstruiert; keine Sonderbehandlung in
`getServiceAdapter`)
- [x] Setup-Guide (PAT-basiert, Calendar + Contacts als zusaetzliche aktive Services)
- [x] PAT-Submit-Endpoint `POST /api/infomaniak/connections/{id}/token`
(Pre-Flight in 2 Schritten: `listAccessibleDrives` (`drive`-
Scope), `resolveOwnerIdentity` (`workspace:calendar` /
`workspace:contact`) -- jeder Schritt mit eigener 400-Message)
- [x] OAuth-Routen entfernt
- [x] Infomaniak-Refresh aus tokenManager + tokenRefreshService entfernt
- [x] Infomaniak-Scopes aus `oauthProviderConfig` entfernt
- [x] `Service_INFOMANIAK_*` aus allen .env-Dateien entfernt
- [x] DataConnections-Dispatch (Infomaniak-Branch in `connect_service` -> 400)
- [x] FE-Hook umgebaut (`createInfomaniakConnection` + `submitInfomaniakToken`)
- [x] FE-OAuth-Popup-Branch fuer Infomaniak entfernt
- [x] ConnectionsPage-PAT-Modal
- [x] UDB-Integration (Authority-Icon, Service-Icons, Source-Colors)
- [x] ClickUp-UDB-Fix
- [ ] Manueller End-to-End-Test (Token im Manager erstellen, in Modal
pasten, kDrive browsen, Calendar browsen, Contacts browsen,
Datei + .ics + .vcf downloaden)
- [x] Mail-API-Pfad verifiziert -- alle 7 erschoepfend getesteten Pfade
scheitern (siehe "Blockiert" oben). Adapter pausiert bis Infomaniak
einen PAT-faehigen Endpoint freischaltet.
- [ ] `wiki/b-reference/connectors.md` (falls vorhanden) ergaenzen
## Akzeptanzkriterien
| # | Kriterium (Given-When-Then) | Prio |
|---|-----------------------------|------|
| 1 | Given Nutzer auf ConnectionsPage, When er auf "Infomaniak" klickt, Then oeffnet sich ein Modal mit Token-Eingabe und Deeplink zu `manager.infomaniak.com/v3/ng/accounts/token/list` | must |
| 2 | Given gueltiger PAT mit Scopes `drive`+(`workspace:calendar` ODER `workspace:contact`) im Modal, When Submit, Then ist die UserConnection `ACTIVE`, Token gespeichert, `externalId=kSuiteAccountId`, `externalUsername=Owner-Anzeigename aus PIM` | must |
| 3 | Given ungueltiger PAT oder fehlender Scope (`drive` ODER weder `workspace:calendar` noch `workspace:contact`), When Submit, Then bleibt Connection PENDING, Modal zeigt 400-Detail das den fehlenden Scope namentlich nennt | must |
| 4 | Given aktive Connection, When im UDB die Authority expandiert wird, Then werden Services `kdrive`, `calendar` und `contact` mit Icons angezeigt | must |
| 4b | Given aktive Connection, When im UDB `kdrive` expandiert wird, Then erscheinen alle Drives die der User sehen kann -- auch wenn er dort nur `role: 'user'` hat (Adapter discovert ueber `/2/drive/init?with=drives`, nicht ueber das admin-only `/2/drive?account_id=...` Listing) | must |
| 4c | Given aktive Connection, When im UDB `calendar` expandiert wird, Then erscheinen Kalender, dann Events; Event-Download liefert `.ics` | must |
| 4d | Given aktive Connection, When im UDB `contact` expandiert wird, Then erscheinen Adressbuecher, dann Kontakte; Kontakt-Download liefert `.vcf` | must |
| 5 | Given Modal offen, When User auf Cancel/X klickt, Then wird die soeben erstellte PENDING-Connection wieder geloescht | should |
| 6 | 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-3 | manual | nein | UI: ConnectionsPage Modal | pending |
| T2 | 4 | manual | nein | UI: UDB SourcesTab | pending |
| T3 | 5 | manual | nein | UI: ConnectionsPage Modal Cancel | pending |
| T4 | 6 | manual | nein | UI: UDB SourcesTab | pending |
## Links
- PR: tbd
- Setup-Guide: `wiki/d-guides/infomaniak-token-setup.md`
## Abschluss
- [ ] `b-reference/` aktualisiert
- [ ] `TOPICS.md` aktualisiert
- [ ] Dokument nach `z-archive/` verschoben