From aeb75af49d546ae37199d79f40a93c24293841a7 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Tue, 21 Apr 2026 10:45:16 +0200
Subject: [PATCH] fix critical trustee db sync
---
d-guides/coding-conventions.md | 68 +++++++++++++++++++++++++++++++++-
1 file changed, 66 insertions(+), 2 deletions(-)
diff --git a/d-guides/coding-conventions.md b/d-guides/coding-conventions.md
index f5f0ff6..6b59ac2 100644
--- a/d-guides/coding-conventions.md
+++ b/d-guides/coding-conventions.md
@@ -1,5 +1,5 @@
-
+
# Coding-Konventionen
@@ -87,6 +87,30 @@ function _tabLabel(tabId: string, t: (k: string) => string): string {
- Fehlende Keys erscheinen als `[Text]` (eckige Klammern = unuebersetzt)
- Admin synchronisiert Sprachsets ueber Administration → System → UI-Sprachen → "Alle aktualisieren"
+#### Platzhalter-Namen sind Code, nicht Text
+
+Die Token in `{...}` (z. B. `{konten}`, `{count}`) sind interne Variablen-Namen
+und werden zur Laufzeit durch das Param-Objekt ersetzt. **Sie duerfen nie
+uebersetzt werden** — auch wenn sie wie deutsche oder englische Woerter aussehen.
+
+```tsx
+// Quelle (immer Deutsch, weil Source-Sprache):
+t('{konten} Konten, {buchungen} Buchungen', { konten: '215', buchungen: '72' })
+
+// Korrekte EN-Uebersetzung in der DB:
+'{konten} accounts, {buchungen} entries'
+// ^^^^^^^ ^^^^^^^^^^^ <-- Token bleiben Deutsch!
+
+// FALSCH (wuerde zu rohem '{accounts}' im UI fuehren):
+'{accounts} accounts, {entries} entries'
+```
+
+Der AI-Uebersetzer verletzt diese Regel ab und zu, deshalb laeuft in
+`routeI18n._enforcePlaceholdersOnBatch` und `i18nRegistry._loadCache` ein
+deterministischer Guard, der bei gleicher Token-Anzahl die Source-Namen
+positionsweise wiederherstellt. Bei abweichender Anzahl bleibt der Wert
+unangetastet — das muss der Reviewer manuell pruefen.
+
## Backend (FastAPI/Python)
- **Pydantic-Models** als einzige Quelle fuer UI-Feld-Definitionen
@@ -94,6 +118,44 @@ function _tabLabel(tabId: string, t: (k: string) => string): string {
- Fehler propagieren -- Exceptions explizit werfen, nicht schlucken
- Config ueber `APP_CONFIG` (aus `modules/shared/configuration.py`)
+### Async-Routen / Background-Jobs: niemals den Event-Loop blockieren
+
+Alle FastAPI-`async def`-Routen und alle Background-Job-Handler
+(`registerJobHandler`) laufen auf dem **gemeinsamen** Event-Loop des Workers.
+Synchroner, blockierender Code (psycopg2, requests, time.sleep, ...) friert
+damit den ganzen Worker ein -- inklusive Health-Checks. Eine grosse Schleife
+mit `db.recordCreate(...)` reicht, um die Instanz fuer Minuten unerreichbar
+zu machen.
+
+**Regeln:**
+
+- HTTP / IO mit nativem `await` aufrufen (httpx, aiohttp, asyncpg, ...).
+- Sync-Code (psycopg2, lange CPU-Schleifen) **immer** in
+ `await asyncio.to_thread(_syncFn, ...)` verpacken.
+- Bei grossen Datenmengen (>100 Zeilen) **niemals** `recordCreate` /
+ `recordDelete` in der Schleife aufrufen. Stattdessen die Bulk-Methoden
+ des Connectors verwenden:
+ - `db.recordCreateBulk(model, rows)` -> ein `execute_values` + ein COMMIT
+ - `db.recordDeleteWhere(model, recordFilter)` -> ein `DELETE WHERE ...`
+- Bei laengeren Phasen Heartbeat-Logs alle 500-1000 Items, damit Hangs
+ im Log sichtbar sind.
+
+Referenz-Implementation: `modules/features/trustee/accounting/accountingDataSync.py`
+(jede Phase: `await connector.fetch(...)` -> `await asyncio.to_thread(self._persistXxx, ...)`).
+
+### Debug-File-Dumps: nur DEV, niemals INT/PROD
+
+Code, der zu Debug-Zwecken raw payloads / Prompts / Sync-Daten auf die Disk
+schreibt, **muss** ein dediziertes Env-Flag pruefen. Pattern:
+
+- `APP_DEBUG__ENABLED` (`True` nur in `env_dev.env`)
+- `APP_DEBUG__DIR` (gateway-relativer Pfad ist OK; absoluter Pfad
+ nur fuer DEV-Workstations)
+
+Beispiele: `APP_DEBUG_CHAT_WORKFLOW_*`, `APP_DEBUG_ACCOUNTING_SYNC_*`. Hardcoded
+Windows-Pfade wie `D:/...` sind verboten (laufen auf Linux als
+Relativ-Pfad an und schreiben unkontrolliert ins Dateisystem).
+
### i18n-Pflicht: `t()` fuer alle UI-sichtbaren Gateway-Texte
Jeder Text der im Frontend angezeigt wird (HTTPException-Details, API-Response-Messages, Erfolgs-/Fehlermeldungen) **muss** mit `t()` getaggt werden (nur **String-Literale** in `t()`).
@@ -142,7 +204,9 @@ def _formatBlockItem(item):
**Warum zwei Schritte?** `t()` wird bei Import-Zeit ausgewertet — der Rueckgabewert ist immer deutsch (Default-Sprache). Der Wert im Dict ist daher ein fester deutscher String. `resolveText()` nimmt diesen deutschen String zur Request-Zeit und liefert die korrekte Uebersetzung.
-**Warum Modul-Ebene?** `t()` registriert Keys nur beim ersten Aufruf. Wenn `t()` nur in einer Funktion steht, wird der Key erst beim ersten Request registriert — **nach** dem Boot-Sync. Der Key fehlt dann im `xx`-Set und kann nicht uebersetzt werden.
+**Warum Modul-Ebene?** `t()` registriert Keys nur beim ersten Aufruf. Wenn `t()` nur in einer Funktion steht, wird der Key erst beim ersten Request registriert — **nach** dem Boot-Sync. Der Key fehlt dann im `xx`-Set und kann nicht uebersetzt werden. Im UI erscheint er als `[Schluessel]` (eckige Klammern = Cache-Miss).
+
+**Pflicht-Hook fuer dynamisch geladene Quellen:** Plugin-/Lazy-Module (z.B. Connector-Plugins, dynamisch entdeckte Klassen) deren `t()`-Aufrufe in einer Methode stecken (nicht auf Modul-Ebene), brauchen einen eigenen `_register…Labels()`-Hook in `_syncRegistryToDb()` (`modules/shared/i18nRegistry.py`). Beispiel: `_registerAccountingConnectorLabels()` importiert die Accounting-Connector-Registry und ruft `getRequiredConfigFields()` einmal beim Boot, damit die Field-Labels im `xx`-Set landen. Analog zu `_registerNodeLabels`, `_registerRbacLabels`, etc.
### TextMultilingual-Felder: Backend loest auf via resolveText()