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()