From 62adb1588365fc195ed7904711337ecab9883d27 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Mon, 25 May 2026 23:10:57 +0200
Subject: [PATCH] fixes
---
b-reference/platform/infrastructure.md | 4 +-
b-reference/platform/nginx-config.md | 117 +++++++++++
.../2026-05-udb-datasource-settings.md | 187 ------------------
...05-int-environment-infomaniak-migration.md | 98 +++++++++
.../2026-05-postgres-connection-pool.md | 0
5 files changed, 217 insertions(+), 189 deletions(-)
create mode 100644 b-reference/platform/nginx-config.md
delete mode 100644 c-work/2-build/2026-05-udb-datasource-settings.md
create mode 100644 c-work/4-done/2026-05-int-environment-infomaniak-migration.md
rename c-work/{2-build => 4-done}/2026-05-postgres-connection-pool.md (100%)
diff --git a/b-reference/platform/infrastructure.md b/b-reference/platform/infrastructure.md
index fd0a463..b153774 100644
--- a/b-reference/platform/infrastructure.md
+++ b/b-reference/platform/infrastructure.md
@@ -49,10 +49,10 @@ Jeder Service-Bereich ist ein eigenes OpenStack-Projekt mit eigenem Netzwerk, ei
|---|---|---|---|---|---|---|
| `porta-main-ui-nyla` | main | Frontend (React/Vite) | a2-ram4-disk20-perf1 | 10.20.0.92 | 37.156.43.5 | `porta.poweron.swiss` |
| `porta-main-platform-core` | main | Backend (FastAPI) | a2-ram4-disk50-perf1 | 10.20.0.197 | 83.228.234.207 | `api.poweron.swiss` |
-| `porta-main-db` | main | PostgreSQL + pgvector | a2-ram4-disk80-perf1 | 10.20.0.21 | 37.156.40.141 | -- |
+| `porta-main-db` | main | PostgreSQL + pgvector | a2-ram4-disk80-perf1 | 10.20.0.21 | 37.156.40.141 | `db.poweron.swiss` |
| `porta-int-ui-nyla` | int | Frontend (React/Vite) | a2-ram4-disk20-perf1 | 10.20.0.182 | 37.156.41.74 | `porta-int.poweron.swiss` |
| `porta-int-platform-core` | int | Backend (FastAPI) | a2-ram4-disk50-perf1 | 10.20.0.74 | 37.156.43.14 | `api-int.poweron.swiss` |
-| `porta-int-db` | int | PostgreSQL + pgvector | a2-ram4-disk80-perf1 | 10.20.0.175 | 37.156.42.67 | -- |
+| `porta-int-db` | int | PostgreSQL + pgvector | a2-ram4-disk80-perf1 | 10.20.0.175 | 37.156.42.67 | `db-int.poweron.swiss` |
Key Pair: `ida-laptop` (alle Instanzen)
diff --git a/b-reference/platform/nginx-config.md b/b-reference/platform/nginx-config.md
new file mode 100644
index 0000000..40bb6d2
--- /dev/null
+++ b/b-reference/platform/nginx-config.md
@@ -0,0 +1,117 @@
+
+
+
+# Nginx-Konfiguration (Reverse Proxy)
+
+Jede `platform-core`-VM (main + int) nutzt nginx als Reverse Proxy vor uvicorn (Port 8000).
+
+## Relevante Einstellungen
+
+| Einstellung | Wert | Zweck |
+|---|---|---|
+| `client_max_body_size` | `0` (unbegrenzt) | Kein Upload-Limit (Dateien, DB-Migration-Restore) |
+| `proxy_pass` | `http://127.0.0.1:8000` | Weiterleitung an uvicorn |
+| `proxy_http_version` | `1.1` | Erforderlich fuer WebSocket-Upgrade |
+| `Upgrade` / `Connection` | `$http_upgrade` / `"upgrade"` | WebSocket-Support (STT-Streaming) |
+| `proxy_read_timeout` | `600s` | Lange AI/STT-Requests |
+| `proxy_send_timeout` | `600s` | Lange Uploads |
+| `proxy_request_buffering` | `off` | Streaming-Uploads ohne Pufferung |
+| SSL | Let's Encrypt (certbot) | TLS-Terminierung |
+
+## Site-Config: porta-main-platform-core
+
+Datei auf VM: `/etc/nginx/sites-enabled/gateway`
+
+```nginx
+server {
+ listen 80;
+ server_name api.poweron.swiss;
+ return 301 https://$host$request_uri;
+}
+
+server {
+ listen 443 ssl;
+ server_name api.poweron.swiss;
+ ssl_certificate /etc/letsencrypt/live/api.poweron.swiss/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/api.poweron.swiss/privkey.pem;
+
+ client_max_body_size 0;
+
+ location / {
+ proxy_pass http://127.0.0.1:8000;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_request_buffering off;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_read_timeout 600s;
+ proxy_send_timeout 600s;
+ }
+}
+```
+
+## Site-Config: porta-int-platform-core
+
+Datei auf VM: `/etc/nginx/sites-enabled/gateway`
+
+```nginx
+server {
+ listen 80;
+ server_name api-int.poweron.swiss;
+ return 301 https://$host$request_uri;
+}
+
+server {
+ listen 443 ssl;
+ server_name api-int.poweron.swiss;
+ ssl_certificate /etc/letsencrypt/live/api-int.poweron.swiss/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/api-int.poweron.swiss/privkey.pem;
+
+ client_max_body_size 0;
+
+ location / {
+ proxy_pass http://127.0.0.1:8000;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_request_buffering off;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_read_timeout 600s;
+ proxy_send_timeout 600s;
+ }
+}
+```
+
+## Einrichtung auf neuer VM
+
+```bash
+# 1. Nginx installieren
+sudo apt update && sudo apt install -y nginx
+
+# 2. Site-Config anlegen
+sudo nano /etc/nginx/sites-enabled/gateway
+# (Inhalt von oben einfuegen)
+
+# 3. Default-Site entfernen
+sudo rm -f /etc/nginx/sites-enabled/default
+
+# 4. nginx.conf: client_max_body_size setzen
+# In /etc/nginx/nginx.conf im http-Block einfuegen:
+# client_max_body_size 0;
+
+# 5. SSL-Zertifikat holen
+sudo apt install -y certbot python3-certbot-nginx
+sudo certbot --nginx -d api-int.poweron.swiss
+
+# 6. Config testen + laden
+sudo nginx -t && sudo systemctl reload nginx
+
+# 7. Auto-Renewal pruefen
+sudo certbot renew --dry-run
+```
diff --git a/c-work/2-build/2026-05-udb-datasource-settings.md b/c-work/2-build/2026-05-udb-datasource-settings.md
deleted file mode 100644
index aa02082..0000000
--- a/c-work/2-build/2026-05-udb-datasource-settings.md
+++ /dev/null
@@ -1,187 +0,0 @@
-
-
-
-
-
-# UDB DataSource Settings (⚙️) + konfigurierbare RAG-Limits
-
-> **Implementierungsstand 2026-05-17:** S0–S11 abgeschlossen. Wichtige Abweichung
-> vom ursprünglichen Plan: das "lazy-fill defaults beim ersten Bootstrap"
-> wurde **nicht** im Walker implementiert (würde die Caller-Override-Semantik
-> aushebeln und liess Tests rot werden). Stattdessen: `_ragLimits` exponiert
-> zwei klare APIs — `getStoredOverrides()` (nur explizite Overrides, für Walker)
-> und `getRagLimits()` (Overrides + Defaults gemerged, für API/Cost-Estimate).
-> Das Modal zeigt fehlende Werte als Placeholder aus dem Cost-Estimate-Endpoint
-> an, damit der User immer eine konkrete Zahl sieht — ohne die Datenbank-Zeile
-> dafür anfassen zu müssen. Siehe CHANGELOG 2026-05-17 für die Detail-Liste.
-
-## Beschreibung und Kontext
-
-Heute sind die RAG-Walker-Limits (`maxBytes`, `maxItems`, `maxFileSize`, `maxDepth`, `maxWorkspaces`, `maxListsPerWorkspace`, `maxTasks`) als Modul-Konstanten in den `subConnectorSync*.py`-Dateien hartkodiert. Ein User mit z. B. einem 500 MB grossen SharePoint-Folder erhält nach 200 MB ein stilles Stoppen (`stoppedAtLimit=maxBytes`) und kann das ohne Code-Änderung nicht anpassen. Heute (2026-05-17) sichtbar geworden auf einem Basecamp-Folder mit ~731 Dateien, von denen nur 25 indexiert wurden, bevor der `maxBytes`-Default zugriff.
-
-Gleichzeitig fehlt für Feature-Instanz-DataSources (`FeatureDataSource`) bisher überhaupt ein UI, an dem der Owner Settings anpassen könnte. UserConnections haben den Wizard und das Edit-Modal auf der Connections-Seite – für Feature-Daten gibt es keinen analogen Ort.
-
-Beide Lücken lassen sich mit derselben Architektur schliessen: **ein generisches Settings-Icon (⚙️) pro Node in der UDB**, das ein typ- und scope-abhängiges Modal öffnet. Initial wird darin RAG-Limit-Werte und Connection-Master-Switch gepflegt, später kommen weitere Einstellungen (z. B. Custom-Neutralization-Regeln, Re-Sync-Cadence, Polling-Strategie) ohne neue Icons dazu.
-
-Wichtiger Architektur-Constraint:
-- **Keine Icon-Inflation in der UDB.** Bereits vorhanden: Scope-Toggle, Neutralize-Toggle, RAG-Index-Toggle (🧠), Chat (💬). Wir fügen genau **ein** zusätzliches Icon (⚙️) hinzu – nicht ein Icon pro Setting.
-
-## Fokus und kritische Details
-
-- UDB ist die **einzige** Stelle, wo Settings pro Datenobjekt gemanagt werden. Andere Pfade (Wizard, Connection-Edit-Modal) editieren nur die Connection-Ebene, nie einzelne DataSources.
-- **Speicherort = Quelle der Wahrheit. Keine Override-Schichten, keine Resolver-Logik.** Was im UI angezeigt wird, ist exakt das, was im Walker greift. Die Werte werden bei der DataSource-Erstellung mit sinnvollen Defaults vorbefüllt (kopiert aus globalen Konstanten); danach kann der User sie einfach editieren. Vorteil: nachvollziehbar, debuggbar, keine versteckten Vererbungen.
-- Walker müssen ihre Default-Konstanten zentralisieren und beim Lesen einer DataSource entweder den dort gespeicherten Wert nehmen oder, falls noch nicht gesetzt, den zentralen Default ausliefern und gleichzeitig auf der DataSource persistieren (lazy initialization → ab dann sind die Werte sichtbar im UI).
-- Settings-Werte landen in einer **JSON-Spalte** auf der DataSource bzw. FeatureDataSource (`settings: Optional[Dict[str, Any]]`) – damit ist die Erweiterbarkeit garantiert, ohne neue Spalten/Migrationen pro neuem Setting.
-- DataSource vs. FeatureDataSource: gleiche Spalte (`settings`), gleiches UI, gleiches API-Pattern (`PATCH /api/datasources/{id}/settings` und `PATCH /api/feature-data-sources/{id}/settings`).
-- Audit-Log-Pflicht: jede Änderung an `settings` schreibt einen `AuditCategory.PERMISSION`-Eintrag (auch wenn es nur ein numerischer Limit-Wert ist – RAG-Ingest-Grenze ist eine Compliance-relevante Entscheidung).
-- Defaults bleiben **konservativ** (200 MB / 10'000 Items / 25 MB / Tiefe 8). Anheben braucht User-Aktion → keine versehentlichen Kostenexplosionen.
-- **Connection-Master-Switch im Modal:** Im Settings-Modal einer DataSource ist auch der Connection-weite Master-Switch `knowledgeIngestionEnabled` ("Knowledge database active") togglebar – damit ist das Modal die zentrale Stelle für alle relevanten Settings, ohne zwischen Seiten wechseln zu müssen. Dieser Toggle wirkt auf alle DataSources derselben Connection (es ist explizit als "Connection-Setting" gekennzeichnet, nicht als DataSource-Setting).
-- **Kosten-Indikator statt Hard-Cap:** Keine harten Obergrenzen. Stattdessen zeigt das Modal eine indikative (nicht-verbindliche) Realtime-Kostenschätzung: basierend auf erwarteter Item-Zahl, Token-Schätzung pro Item und aktuellen Embedding-Preisen. Klar als "indikativ" gekennzeichnet, der User trägt die Verantwortung.
-
-## Ziel und Nicht-Ziele
-
-- **Ziel:**
- - Ein Settings-Icon ⚙️ pro Tree-Node in der UDB-Sidebar (`SourcesTab.tsx`), das ein Modal öffnet.
- - Im Modal: drei klar abgegrenzte Sektionen:
- 1. **Connection** (Master-Switch `knowledgeIngestionEnabled`, gemeinsame Mail-/ClickUp-Preferences aus `knowledgePreferences`).
- 2. **DataSource RAG-Limits** (`maxBytes`, `maxFileSize`, `maxItems`, `maxDepth`).
- 3. **Kostenschätzung** (indikativer Wert mit Hinweis).
- - Backend: `DataSource.settings` und `FeatureDataSource.settings` als JSON-Spalte. Endpunkte zum Lesen/Patchen.
- - Walker (`subConnectorSyncSharepoint.py`, `subConnectorSyncKdrive.py`, `subConnectorSyncGdrive.py`, `subConnectorSyncClickup.py`) lesen Limits direkt aus `DataSource.settings.ragLimits`, fallen auf zentrale Defaults zurück, wenn (noch) leer.
- - Partial-Banner auf `RagInventoryPage` zeigt zusätzlich Hint: "Limit kann pro DataSource in der UDB unter ⚙️ angehoben werden."
- - Owner-Kontrolle: für UserConnection-DataSources nur Owner; für FeatureDataSource Owner oder `workspace-admin`.
-- **Nicht-Ziel:**
- - Mandate-weite Defaults / Override-Schichten / Resolver-Layer.
- - Hard-Caps (User/Admin trägt Verantwortung).
- - Settings-Vererbung im Tree (Parent-Folder → Children) – aktuell wirkt eine Änderung nur auf die konkrete DataSource.
- - Eigene Settings für Mail-Connectors (`Outlook`, `Gmail`) auf DataSource-Ebene – die haben keine Folder-Hierarchie. Die Mail-Preferences bleiben Connection-weit und werden im Modal in der "Connection"-Sektion editiert.
-
-## Architektur-Skizze
-
-### Daten-Schicht
-
-`DataSource` und `FeatureDataSource` bekommen je eine neue Spalte:
-```python
-settings: Optional[Dict[str, Any]] = Field(
- default=None,
- description="DataSource-scoped settings (JSON). Currently used keys: ragLimits.",
- json_schema_extra={"frontend_type": "json", "frontend_readonly": True, "frontend_required": False},
-)
-```
-
-JSON-Schema-Konvention:
-```json
-{
- "ragLimits": {
- "maxBytes": 524288000,
- "maxFileSize": 52428800,
- "maxItems": 20000,
- "maxDepth": 12
- }
-}
-```
-**Keine Resolver-Schichten**: was in `settings.ragLimits` steht, gilt. Wenn der Key fehlt, nimmt der Walker den zentralen Default aus `_ragLimits.RAG_LIMITS_DEFAULT` und schreibt ihn beim nächsten Bootstrap **einmalig** in die DataSource zurück (lazy fill), damit der User die Werte auch ohne vorherigen Sync schon im UI sieht und editieren kann.
-
-### Backend-API
-
-```
-PATCH /api/datasources/{id}/settings { settings: { ragLimits: {...} } }
-PATCH /api/feature-data-sources/{id}/settings { settings: { ragLimits: {...} } }
-GET /api/datasources/{id}/cost-estimate → { estimatedTokens, estimatedChf, basis: {...} }
-```
-`/cost-estimate`: schätzt anhand Item-Count (sofern bekannt aus letztem Sync) × tokens-per-item-heuristik × Embedding-Preis. Liefert auch die Annahmen (`basis`), damit der User die Plausibilität prüfen kann.
-
-### Walker-Refactor
-
-Heute hartkodiert in jedem Walker:
-```python
-MAX_BYTES_DEFAULT = 200 * 1024 * 1024
-MAX_ITEMS_DEFAULT = 10_000
-```
-Wird zu:
-```python
-# gateway/modules/serviceCenter/services/serviceKnowledge/_ragLimits.py
-RAG_LIMITS_DEFAULT = {
- "maxBytes": 200 * 1024 * 1024,
- "maxFileSize": 25 * 1024 * 1024,
- "maxItems": 10_000,
- "maxDepth": 8,
-}
-
-def getRagLimits(dataSource: Dict[str, Any]) -> Dict[str, int]:
- """Read limits from DataSource.settings.ragLimits, fall back to defaults
- for missing keys. Pure read; lazy persist is the caller's responsibility."""
- stored = (dataSource.get("settings") or {}).get("ragLimits") or {}
- return {**RAG_LIMITS_DEFAULT, **stored}
-
-def lazyFillRagLimits(rootIf, dataSourceId: str, dataSource: Dict[str, Any]) -> None:
- """Persist defaults to settings if not yet present, so the UI shows them."""
-```
-Jeder Walker holt seine Limits beim Eintritt in `_bootstrap*` einmal und gibt sie an `_walkFolder()` / `_finalizeResult()` weiter – die Konstante existiert nur noch im neuen Modul als Default-Fallback.
-
-### Frontend-UI
-
-In `SourcesTab.tsx` (Tree-Node-Render), zwischen Brain-Icon (🧠) und Chat-Icon (💬):
-```typescript
-
-```
-
-Settings-Modal (`DataSourceSettingsModal.tsx`):
-- **Sektion "Connection"** (oben, mit Connection-Label und Authority-Icon):
- - Toggle `knowledgeIngestionEnabled` (Master-Switch). Hinweis: wirkt auf alle DataSources dieser Connection.
- - Optional, wenn relevant: `mailContentDepth`, `mailIndexAttachments`, `filesIndexBinaries`, `clickupScope`, `clickupIndexAttachments`, `maxAgeDays` (aus `knowledgePreferences`).
- - PATCH → `/api/connections/{id}/knowledge-consent` bzw. `/api/connections/{id}/knowledge-preferences`.
-- **Sektion "RAG-Limits"** (nur wenn DataSource den Walker-Typ unterstützt):
- - Felder `maxBytes`, `maxFileSize`, `maxItems`, `maxDepth` mit lesbaren Units (MB, Anzahl).
- - PATCH → `/api/datasources/{id}/settings`.
-- **Sektion "Kostenschätzung"** (read-only):
- - "Indikative Kosten: ~X CHF pro Voll-Sync" (mit Basis-Tooltip).
- - GET → `/api/datasources/{id}/cost-estimate` beim Modal-Öffnen.
-
-Auf der `RagInventoryPage`-Partial-Banner-Komponente:
-```typescript
-... Limit {l} erreicht. Weitere Dateien wurden NICHT indexiert. {' '}
- openSettingsModalForConn(conn)}>Limit anpassen oder
- DataSource enger eingrenzen, dann erneut starten.
-```
-
-## Erfolgskriterien
-
-- Auf einer SharePoint-DataSource mit > 200 MB kann der Owner über das ⚙️-Icon `maxBytes` auf z. B. 1 GB anheben, einen Re-Index starten und sieht im Inventory > 200 MB indexierte Bytes ohne `stoppedAtLimit`-Banner.
-- Auf einer FeatureDataSource ist das gleiche Settings-Modal verfügbar und speichert sauber in `FeatureDataSource.settings`.
-- Wenn `settings.ragLimits` leer ist, ist das Verhalten der Walker **bitidentisch** zur Vor-Plan-Version (keine Regression).
-- Der Connection-Master-Switch im Modal und der Toggle auf der Connections-Page und auf der RagInventoryPage zeigen immer denselben Wert (alle drei rufen `/knowledge-consent`).
-- Audit-Log enthält pro Settings-Change einen `PERMISSION`-Eintrag mit `dataSourceId`, `userId`, `mandateId`, `oldSettings`, `newSettings`.
-- Keine zusätzlichen Icons in der UDB ausser dem einen ⚙️.
-- Kostenschätzung wird als "indikativ, nicht verbindlich" gekennzeichnet und zeigt die zugrunde liegenden Annahmen.
-
-## Schritte
-
-- [ ] S1 — Datenmodell: `settings: JSON` an `DataSource` und `FeatureDataSource` ergänzen (`gateway/modules/datamodels/datamodelDataSource.py`, `datamodelFeatureDataSource.py`); SQL-Migration in `gateway/scripts/script_db_migrate_datasource_settings.py`.
-- [ ] S2 — Zentralisierte Defaults: `gateway/modules/serviceCenter/services/serviceKnowledge/_ragLimits.py` neu mit `RAG_LIMITS_DEFAULT`, `getRagLimits()`, `lazyFillRagLimits()`.
-- [ ] S3 — Walker-Refactor: `subConnectorSyncSharepoint.py`, `subConnectorSyncKdrive.py`, `subConnectorSyncGdrive.py`, `subConnectorSyncClickup.py` lesen Limits via `getRagLimits()` und schreiben Defaults via `lazyFillRagLimits()` zurück.
-- [ ] S4 — Backend-Endpunkte: `PATCH /api/datasources/{id}/settings`, `PATCH /api/feature-data-sources/{id}/settings`, `GET /api/datasources/{id}/cost-estimate` in `routeDataSources.py` (und `routeFeatureDataSources.py`, falls noch nicht da). RBAC: Owner für DataSource, Owner + `workspace-admin` für FeatureDataSource.
-- [ ] S5 — Frontend ⚙️-Button in `SourcesTab.tsx` (Tree-Row, zwischen 🧠 und 💬), Opacity-Logik: voll sichtbar, wenn `settings` befüllt sind, sonst gedimmt.
-- [ ] S6 — Frontend `DataSourceSettingsModal.tsx`: drei Sektionen (Connection / RAG-Limits / Kostenschätzung), per-Feld-Validation, Speichern via API.
-- [ ] S7 — `RagInventoryPage.tsx` Partial-Banner: Link "Limit anpassen" öffnet Modal für die betroffene DataSource (Heuristik: DS, die `stoppedAtLimit` ausgelöst hat – bei mehreren: die mit den meisten verarbeiteten Bytes).
-- [ ] S8 — Audit-Logging in den neuen Settings-Endpunkten.
-- [ ] S9 — Cost-Estimate-Engine: `gateway/modules/serviceCenter/services/serviceKnowledge/_costEstimate.py` mit Heuristik (Items × Tokens × Embedding-Preis) und Basis-Annahmen-Output.
-- [ ] S10 — Tests: Unit-Tests für `getRagLimits()` / `lazyFillRagLimits()`, API-Tests für PATCH-Endpunkte (RBAC), Cost-Estimate-Unit-Test, Frontend-Smoke-Test (Modal öffnet, speichert, refetch, Cost-Anzeige).
-- [ ] S11 — Doku: `wiki/b-reference/platform/rag-pipeline.md` Abschnitt "Limits & Settings" ergänzen, plus Eintrag in `wiki/TOPICS.md`. CHANGELOG-Zeile.
-
-## Offene Fragen / Decisions
-
-- **F1 – Override-Logik?** → **Entschieden: KEINE Override-Schichten.** Die DataSource speichert ihre eigenen Werte direkt; Walker liest sie 1:1. Lazy-Fill mit zentralen Defaults beim ersten Sync, damit das UI immer einen sinnvollen Startwert zeigt. Nachvollziehbar, debuggbar, keine versteckten Vererbungen.
-- **F2 – RBAC?** → **Entschieden:** Owner für UserConnection-DataSources, Owner oder `workspace-admin` für FeatureDataSource (analog Scope-/Neutralize-Toggle).
-- **F3 – Hard-Cap pro Mandat?** → **Entschieden: NEIN.** Stattdessen indikative Realtime-Kostenschätzung im Modal. User/Admin trägt Verantwortung.
-- **F4 – Mail/ClickUp-Preferences im Modal?** → **Entschieden: JA.** Connection-weite Preferences werden in der Sektion "Connection" des Modals editiert – das macht das Modal zur einzigen Stelle für alle relevanten Settings, ohne Seitenwechsel.
diff --git a/c-work/4-done/2026-05-int-environment-infomaniak-migration.md b/c-work/4-done/2026-05-int-environment-infomaniak-migration.md
new file mode 100644
index 0000000..f0742dd
--- /dev/null
+++ b/c-work/4-done/2026-05-int-environment-infomaniak-migration.md
@@ -0,0 +1,98 @@
+
+
+
+
+# INT-Umgebung auf Infomaniak migrieren
+
+## Beschreibung und Kontext
+
+Migration der `poweron-swiss` INT-Umgebung (platform-core, ui-nyla) von Azure auf Infomaniak Public Cloud.
+Betrifft nur `poweron-swiss`-Repos. Die `poweron`-Repos (gateway, frontend_nyla) bleiben auf Azure.
+
+## Checkliste
+
+### Env-Dateien (platform-core, ui-nyla)
+
+- [x] `env-int.env`: APP_API_URL → `https://api-int.poweron.swiss`
+- [x] `env-int.env`: DB_HOST → `db-int.poweron.swiss`
+- [x] `env-int.env`: DB_USER → `poweron_dev`
+- [x] `env-int.env`: OAuth-Redirect-URIs → `api-int.poweron.swiss`
+- [x] `env-int.env`: CORS (APP_ALLOWED_ORIGINS) → alle UI-Domains (poweron.swiss + poweron-center.net)
+- [x] `env-int.env`: APP_KEY_SYSVAR → `/srv/gateway/shared/secrets/master_key.txt`
+- [x] `env-int.env`: APP_LOGGING_LOG_DIR → `srv/gateway/shared/logs`
+- [x] `env-int.env`: TEAMSBOT_BROWSER_BOT_URL → `http://teamsbot.poweron.swiss:4100`
+- [x] `env-prod.env`: DB_HOST → `db.poweron.swiss`
+- [x] `env-prod.env`: CORS aktualisiert
+- [x] `env-prod.env`: TEAMSBOT_URL aktualisiert
+- [x] `ui-nyla` env-int.env: bereits korrekt (`VITE_API_BASE_URL=https://api-int.poweron.swiss`)
+
+### OAuth-Provider (externe Portale)
+
+- [x] Microsoft Entra: Redirect-URIs auf `api-int.poweron.swiss` aktualisiert
+- [x] Google OAuth Console: Redirect-URIs auf `api-int.poweron.swiss` aktualisiert
+
+### DNS
+
+- [x] `db.poweron.swiss` → 10.20.0.21 (porta-main-db)
+- [x] `db-int.poweron.swiss` → 10.20.0.175 (porta-int-db)
+- [x] `teamsbot.poweron.swiss` → eingerichtet
+- [x] `api-int.poweron.swiss` → eingerichtet
+
+### Server: porta-int-db (db-int.poweron.swiss)
+
+SSH: `ssh -i ~/.ssh/ida-laptop.pem ubuntu@37.156.42.67`
+
+- [x] VM läuft, SSH-Verbindung OK
+
+**Schritt 1: Bestand prüfen**
+
+- [x] PostgreSQL-Status prüfen
+ ```
+ sudo systemctl status postgresql
+ ```
+- [x] Falls PostgreSQL läuft: vorhandene User prüfen
+ ```
+ sudo -u postgres psql -c "\du" --> USER poweron_dev exists
+ ```
+- [x] Vorhandene Datenbanken prüfen
+ ```
+ sudo -u postgres psql -c "\l"
+ ```
+- [x] pg_hba.conf prüfen (Zugriff vom internen Subnetz?)
+ ```
+ sudo cat /etc/postgresql/*/main/pg_hba.conf | grep -v "^#" | grep -v "^$"
+ ```
+
+**Schritt 2: Passwort + Zugriff**
+
+- [x] Passwort für `poweron_dev` gesetzt
+- [x] Verbindungstest lokal auf DB-VM OK (`psql -h 127.0.0.1 -U poweron_dev -d postgres -c "SELECT 1;"`)
+
+### Server: porta-int-platform-core (api-int.poweron.swiss)
+
+- [x] Verbindungstest zur DB OK
+- [x] Master Key abgelegt (`/srv/gateway/shared/secrets/master_key.txt`)
+- [x] Log-Verzeichnis vorhanden
+- [x] App-Verzeichnis + Git-Repo vorhanden
+- [x] Python venv vorhanden
+- [x] systemd-Service `gateway` konfiguriert
+- [x] Nginx: `client_max_body_size 0`, WebSocket-Upgrade, Timeouts 600s, `proxy_request_buffering off` (analog PROD)
+- [x] SSL: Let's Encrypt Zertifikat fuer `api-int.poweron.swiss` aktiv
+
+### DB-Passwort verschlüsseln
+
+- [x] Passwort in `env-int.env` eingetragen und verschlüsselt (`DB_PASSWORD_SECRET = INT_ENC:...`)
+
+### Forgejo CI/CD
+
+- [x] Workflow `int_porta-int-platform-core.yml` geprüft — zeigt korrekt auf `api-int.poweron.swiss`, Branch `int`, Pfad `/srv/gateway/current`
+
+### Wiki
+
+- [x] DNS-Einträge für DB-Server in `infrastructure.md` nachgetragen
+- [x] CHANGELOG aktualisiert
+
+## Notizen
+
+- SSH auf porta-int-db geht nur über öffentliche IP `37.156.42.67` (DNS `db-int.poweron.swiss` zeigt auf interne IP 10.20.0.175, korrekt für App-Zugriff)
+- SSH auf porta-main-db: `37.156.40.141`
diff --git a/c-work/2-build/2026-05-postgres-connection-pool.md b/c-work/4-done/2026-05-postgres-connection-pool.md
similarity index 100%
rename from c-work/2-build/2026-05-postgres-connection-pool.md
rename to c-work/4-done/2026-05-postgres-connection-pool.md