From cf2e8759682cc5392f6dff362cdda70e5306eb0a Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Tue, 14 Apr 2026 00:15:28 +0200
Subject: [PATCH] =?UTF-8?q?u=C3=BCd?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
b-reference/frontend-nyla/architecture.md | 4 +-
b-reference/frontend-nyla/formgenerator.md | 208 ++++++++++++++++
...026-04-database-health-and-data-cleanup.md | 228 ++++++++++++++++++
z-archive/post-cleanup-report-2026-04-12.md | 16 +-
4 files changed, 448 insertions(+), 8 deletions(-)
create mode 100644 b-reference/frontend-nyla/formgenerator.md
create mode 100644 c-work/1-plan/2026-04-database-health-and-data-cleanup.md
diff --git a/b-reference/frontend-nyla/architecture.md b/b-reference/frontend-nyla/architecture.md
index 5430185..7bed8e8 100644
--- a/b-reference/frontend-nyla/architecture.md
+++ b/b-reference/frontend-nyla/architecture.md
@@ -1,5 +1,5 @@
-
+
# Frontend Nyla -- Architektur
@@ -32,7 +32,7 @@ Ergänzend typische Root-Dateien und Bereiche im Repo: `main.tsx`, `App.tsx`, `a
| Komponente | Zweck |
|------------|-------|
| `UnifiedDataBar (UDB)` | Multi-Tab-Panel: Chats, Files, Sources — wird in Workspace, CommCoach und Graphical Editor genutzt |
-| `FormGenerator` | Dynamische Formulare/Tabellen aus Backend-Attribut-Definitionen |
+| `FormGenerator` | Dynamische Formulare/Tabellen aus Backend-Attribut-Definitionen (siehe [formgenerator.md](formgenerator.md)) |
| `FolderTree` | Rekursiver Ordnerbaum mit Drag&Drop, Multi-Selection, Inline-Editing |
| `MandateNavigation` | Feature-Baum-Navigation mit Mandanten-Kontext |
| `Automation2FlowEditor` | Konsolidierter n8n-style Flow-Builder (Graphical Editor) mit 3-Column-Layout: [UDB + Chat/Tracing] [Canvas + Header] [Nodes] |
diff --git a/b-reference/frontend-nyla/formgenerator.md b/b-reference/frontend-nyla/formgenerator.md
new file mode 100644
index 0000000..eb41926
--- /dev/null
+++ b/b-reference/frontend-nyla/formgenerator.md
@@ -0,0 +1,208 @@
+
+
+
+
+# FormGenerator -- Referenz
+
+## Uebersicht
+
+Der FormGenerator besteht aus mehreren Komponenten:
+
+| Komponente | Datei | Zweck |
+|------------|-------|-------|
+| `FormGeneratorTable` | `components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx` | Generische Datentabelle mit Pagination, Sortierung, Filterung, Suche, Selektion, Bulk-Actions |
+| `FormGeneratorControls` | `components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx` | Toolbar: Suche, Pagination, Batch-Action-Buttons, "Select All Filtered"-Banner |
+| `FormGeneratorForm` | `components/FormGenerator/FormGeneratorForm/FormGeneratorForm.tsx` | Dynamische Formulare aus Backend-Attribut-Definitionen |
+| `FormGeneratorList` | `components/FormGenerator/FormGeneratorList/FormGeneratorList.tsx` | Listen-Darstellung |
+
+Diese Referenz fokussiert auf **FormGeneratorTable** und das zugehoerige Backend-API-Pattern.
+
+---
+
+## FormGeneratorTable -- Props
+
+### Datenanbindung
+
+| Prop | Typ | Beschreibung |
+|------|-----|-------------|
+| `data` | `T[]` | Anzuzeigende Datensaetze (aktuelle Seite) |
+| `columns` | `ColumnConfig[]` | Spaltendefinitionen (key, label, type, sortable, filterable, searchable, filterOptions, width) |
+| `apiEndpoint` | `string?` | Backend-Endpunkt fuer automatische Filter-/ID-Abfragen |
+| `hookData` | `{ refetch, pagination, fetchFilterValues? }` | Daten-Hook mit Refetch-Callback und Pagination-Metadata |
+| `supportsBackendPagination` | `boolean` | Aktiviert serverseitige Pagination, Sortierung und Filterung |
+
+### Interaktion
+
+| Prop | Typ | Beschreibung |
+|------|-----|-------------|
+| `selectable` | `boolean` | Aktiviert Zeilen-Selektion (Checkboxen) |
+| `onSelectionChange` | `(selectedIds: Set) => void` | Callback bei Aenderung der Selektion |
+| `idField` | `string` | Feld fuer eindeutige ID (Default: `"id"`) |
+| `isRowSelectable` | `(row: T) => boolean` | Bestimmt, ob eine Zeile selektierbar ist |
+| `batchActions` | `BatchAction[]` | Bulk-Aktionen fuer selektierte Zeilen |
+| `actionButtons` | `ActionButton[]` | Zeilen-Aktionen (Edit, Delete etc.) |
+
+### BatchAction Interface
+
+```typescript
+interface BatchAction {
+ label: string;
+ icon?: React.ReactNode;
+ loading?: boolean;
+ onClick: (selectedRows: T[]) => void | Promise;
+ isApplicable?: (row: T) => boolean;
+}
+```
+
+`isApplicable` ermoeglicht pro Action zu bestimmen, welche der selektierten Zeilen fuer diese Aktion qualifizieren. Der Button zeigt dann `(applicableCount/totalSelected)` und ist `disabled` wenn `applicableCount === 0`.
+
+---
+
+## Unified Filter API
+
+### Prinzip
+
+Alle Daten-Endpunkte unterstuetzen drei Modi ueber Query-Parameter auf dem **gleichen** Endpunkt:
+
+| Modus | Query-Parameter | Response | Zweck |
+|-------|----------------|----------|-------|
+| Normal (default) | `pagination={...}` | `{ items: T[], pagination: PaginationMetadata }` | Paginierte Datenliste |
+| `filterValues` | `mode=filterValues&column=xxx&pagination={crossFilters}` | `string[]` | Distinct-Werte fuer Spaltenfilter-Dropdown |
+| `ids` | `mode=ids&pagination={filters}` | `string[]` | Alle IDs der gefilterten Datensaetze |
+
+**Kein separater `/filter-values`-Endpunkt.** Alle alten `/filter-values`-Sub-Pfade wurden entfernt (Stand 2026-04-13).
+
+### Cross-Filtering
+
+Bei `mode=filterValues` werden alle aktiven Filter **ausser** dem angeforderten Column-Filter angewendet (Cross-Filtering). So zeigt z.B. der Status-Filter nur Werte an, die bei den aktuell gesetzten Mandant- und Typ-Filtern vorkommen.
+
+### Beispiele
+
+```
+# Normale Liste (Seite 1, 20 Eintraege)
+GET /api/users/?pagination={"page":1,"pageSize":20,"sort":[{"field":"name","direction":"asc"}]}
+
+# Filter-Werte fuer Spalte "status" (mit Cross-Filter auf mandateLabel)
+GET /api/users/?mode=filterValues&column=status&pagination={"filters":{"mandateLabel":"Demo AG"}}
+
+# Alle IDs der gefilterten Ansicht (fuer "Select All Filtered")
+GET /api/users/?mode=ids&pagination={"filters":{"status":"active"},"search":"admin"}
+```
+
+### Backend-Implementierung
+
+Zentrale Hilfsfunktionen in `gateway/modules/routes/routeHelpers.py`:
+
+| Funktion | Zweck |
+|----------|-------|
+| `handleFilterValuesMode(db, modelClass, column, ...)` | SQL-basierte Distinct-Werte via DB-Connector |
+| `handleIdsMode(db, modelClass, ...)` | SQL-basierte ID-Liste via DB-Connector |
+| `handleFilterValuesInMemory(items, column, ...)` | In-Memory Distinct-Werte (fuer enriched/aggregierte Listen) |
+| `handleIdsInMemory(items, ...)` | In-Memory ID-Liste |
+| `parseCrossFilterPagination(column, paginationJson)` | Parsed Pagination-JSON und entfernt den angeforderten Column-Filter |
+| `_applyFiltersAndSort(items, paginationParams)` | In-Memory Filterung und Sortierung |
+| `_extractDistinctValues(items, column, ...)` | Distinct-Werte aus Item-Liste extrahieren |
+| `paginateInMemory(items, paginationParams)` | In-Memory Paginierung |
+
+### Route-Integration Pattern
+
+```python
+@router.get("/", response_model=PaginatedResponse[MyModel])
+def list_items(
+ request: Request,
+ pagination: Optional[str] = Query(None),
+ mode: Optional[str] = Query(None, description="'filterValues' or 'ids'"),
+ column: Optional[str] = Query(None, description="Column key"),
+ context: RequestContext = Depends(getRequestContext),
+):
+ if mode in ("filterValues", "ids"):
+ from modules.routes.routeHelpers import handleFilterValuesInMemory, handleIdsInMemory
+ items = _buildFullList(context)
+ if mode == "filterValues":
+ if not column:
+ raise HTTPException(status_code=400, detail="column required")
+ return handleFilterValuesInMemory(items, column, pagination)
+ return handleIdsInMemory(items, pagination)
+
+ # Normal paginated list...
+```
+
+---
+
+## Selektion
+
+### ID-basierte Selektion
+
+Die Selektion arbeitet mit **IDs** (nicht Zeilen-Indizes). Das `idField`-Prop bestimmt, welches Feld als eindeutiger Schluessel verwendet wird (Default: `"id"`).
+
+| State | Typ | Beschreibung |
+|-------|-----|-------------|
+| `selectedIds` | `Set` | Menge der selektierten IDs |
+
+### Selektion-Reset
+
+Die Selektion wird automatisch zurueckgesetzt bei:
+- Seitenwechsel (currentPage)
+- Seitengroesse-Aenderung (pageSize)
+- Filter-Aenderung
+- Suchbegriff-Aenderung
+- Sortier-Aenderung
+
+### Select All Filtered
+
+Wenn `apiEndpoint` und `supportsBackendPagination` gesetzt sind, zeigt die Toolbar einen "Alle X Eintraege auswaehlen"-Button. Dieser ruft `GET {apiEndpoint}?mode=ids&pagination={currentFilters}` auf und fuegt alle zurueckgegebenen IDs zur Selektion hinzu.
+
+Ein "Auswahl aufheben"-Button erscheint, wenn "Select All Filtered" aktiv ist.
+
+### Row-Level Delete
+
+Wenn eine Zeile geloescht wird (via `actionButtons` Delete), wird deren ID automatisch aus `selectedIds` entfernt.
+
+---
+
+## FilterValuesList (Spaltenfilter-Dropdown)
+
+Wenn ein filterbarer Spalten-Header geklickt wird, laedt das Dropdown die Distinct-Werte:
+
+1. `column.filterOptions` (statische Enum) -- wird direkt verwendet, kein Backend-Call
+2. `hookData.fetchFilterValues(columnKey, crossFilters)` -- falls im Hook bereitgestellt
+3. `GET {apiEndpoint}?mode=filterValues&column=xxx&pagination={currentFilters}` -- automatisch
+
+Bei mehr als 10 Werten erscheint ein **Suchfeld** (Client-seitige Filterung der geladenen Werte).
+
+Boolean-Spalten rendern als "Ja"/"Nein". Datum-Spalten rendern als Range-Picker.
+
+---
+
+## Migrierte Endpunkte (Stand 2026-04-13)
+
+Alle folgenden Endpunkte unterstuetzen `mode=filterValues` und `mode=ids`:
+
+### Core Routes
+
+| Route-Datei | Endpunkt | Methode |
+|-------------|----------|---------|
+| `routeDataUsers.py` | `GET /api/users/` | SQL + In-Memory (je nach Scope) |
+| `routeDataConnections.py` | `GET /api/connections/` | In-Memory |
+| `routeDataPrompts.py` | `GET /api/prompts/` | In-Memory |
+| `routeInvitations.py` | `GET /api/invitations/` | In-Memory |
+| `routeSubscription.py` | `GET /api/subscriptions/admin/all` | In-Memory |
+| `routeAdminFeatures.py` | `GET /api/admin/features/instances` | In-Memory |
+| `routeAdminFeatures.py` | `GET /api/admin/features/templates/roles` | In-Memory |
+| `routeAdminFeatures.py` | `GET /api/admin/features/instances/{id}/users` | In-Memory |
+| `routeAdminRbacRules.py` | `GET /api/admin/rbac/roles` | In-Memory |
+| `routeDataMandates.py` | `GET /api/mandates/` | SQL + In-Memory (je nach Scope) |
+| `routeDataMandates.py` | `GET /api/mandates/{id}/users` | In-Memory |
+| `routeDataFiles.py` | `GET /api/files/list` | SQL + In-Memory Fallback |
+| `routeWorkflowDashboard.py` | `GET /api/automation/dashboard/` (Runs) | Enriched + SQL |
+| `routeWorkflowDashboard.py` | `GET /api/automation/dashboard/workflows` | Enriched + SQL |
+| `routeBilling.py` | `GET /api/billing/view/users/transactions` | Billing-Interface |
+
+### Feature Routes
+
+| Route-Datei | Endpunkt | Methode |
+|-------------|----------|---------|
+| `routeFeatureTrustee.py` | `GET /api/trustee/{id}/documents` | RBAC SQL + In-Memory Fallback |
+| `routeFeatureTrustee.py` | `GET /api/trustee/{id}/positions` | RBAC SQL + In-Memory Fallback |
+| `routeFeatureRealEstate.py` | `GET /api/realestate/{id}/projects` | In-Memory |
+| `routeFeatureRealEstate.py` | `GET /api/realestate/{id}/parcels` | In-Memory |
diff --git a/c-work/1-plan/2026-04-database-health-and-data-cleanup.md b/c-work/1-plan/2026-04-database-health-and-data-cleanup.md
new file mode 100644
index 0000000..3133822
--- /dev/null
+++ b/c-work/1-plan/2026-04-database-health-and-data-cleanup.md
@@ -0,0 +1,228 @@
+
+
+
+
+# Database Health and Data Cleanup — Admin Page
+
+## Beschreibung und Kontext
+
+SysAdmin-Seite unter Admin > System > "Datenbank-Gesundheit" mit zwei Views:
+
+1. **Tabellenstatistiken** — Row Count, Size (MB), Index Size, Last Vacuum/Analyze fuer alle Tabellen in allen Datenbanken
+2. **Orphan Cleanup** — Generische Erkennung verwaister Datensaetze (FK-Referenzen auf nicht-existierende Parent-Records) mit Cleanup-Buttons
+
+**Business-Treiber:** Beim Entfernen von Demo-Sets (und generell beim Loeschen von Mandanten/Features) bleiben verwaiste Daten im System. Aktuell gibt es keine Sicht auf den Zustand der Datenbanken und keine Moeglichkeit, Orphans systematisch zu erkennen und zu bereinigen.
+
+**Abhaengigkeiten:**
+- FormGeneratorTable mit Batch-Handling (wird parallel implementiert)
+- Bestehende Interface-Architektur (interfaceDb*, interfaceFeature*)
+
+**Risiko bei Nicht-Umsetzung:** Verwaiste Daten akkumulieren sich ueber die Zeit, belegen Speicher, und koennen zu Inkonsistenzen fuehren (z.B. Workflows ohne Mandate, Feature-Configs ohne Feature-Instanzen).
+
+## Fokus und kritische Details
+
+### Dynamische DB-Registry (Selbst-Registrierung)
+
+Jede Datenbank registriert sich **selbst** in ihrem Interface (`interfaceDb*.py` oder `interfaceFeature*.py`). Es gibt **keine** statische Liste aller Datenbanken. Neue DBs werden automatisch erkannt, entfernte DBs verschwinden.
+
+**Pattern:** Jedes Interface ruft beim Import oder bei der Initialisierung `_registerDatabase("poweron_xyz")` auf. Die Registry sammelt alle registrierten DBs dynamisch.
+
+### Deklaratives FK-Registry auf Model-Ebene
+
+Jedes `*Id`-Feld das eine echte FK-Beziehung darstellt, bekommt ein `fk_target` in `json_schema_extra`:
+
+```python
+mandateId: str = Field(
+ ...,
+ json_schema_extra={
+ ...,
+ "fk_target": {"db": "poweron_app", "table": "Mandate"},
+ },
+)
+```
+
+Felder die **keine** DB-FK sind (Stripe-IDs, logische IDs, Graph-Node-IDs, polymorphe `referenceId`) bekommen **kein** `fk_target`.
+
+### Cross-DB Orphan Detection
+
+Viele FK-Beziehungen gehen ueber Datenbank-Grenzen hinweg (z.B. `AutoWorkflow.featureInstanceId` in `poweron_graphicaleditor` referenziert `FeatureInstance` in `poweron_app`). Der Scanner muss Parent-IDs aus der Ziel-DB laden und dann im Child-DB pruefen.
+
+### Performance
+
+- **Stats:** `pg_stat_user_tables` + `pg_total_relation_size` sind PostgreSQL-Katalog-Queries (kein Table-Scan). Ueber ~14 DBs mit je 5-15 Tabellen dauert der Call wenige Sekunden.
+- **Orphans:** Cross-DB-Checks koennen bei grossen Tabellen langsam sein. Ergebnisse werden gecacht (5 Min TTL).
+- **`n_live_tup`** ist ein Schaetzwert (aktualisiert nach ANALYZE/VACUUM). Fuer ein Health-Dashboard ausreichend.
+
+## Ziel und Nicht-Ziele
+
+**Ziel:**
+- Dynamische DB-Registry die sich aus den Interfaces selbst befuellt
+- Auto-Registry fuer PowerOnModel-Subklassen (Tabellenname = Klassenname)
+- `fk_target` Annotationen auf allen echten FK-Feldern
+- Generischer Orphan-Scanner der FK-Graph aus Model-Metadaten aufbaut
+- Tabellenstatistiken via PostgreSQL-Katalog
+- SysAdmin-UI mit zwei Tabs (Stats + Orphans) und Clean-Buttons
+- Navigation-Eintrag unter Admin > System
+
+**Explizit NICHT:**
+- Keine automatische Bereinigung (nur manuell per Button)
+- Keine Schema-Migration oder Aenderung an bestehenden DB-Constraints
+- Kein Monitoring/Alerting (nur On-Demand-Scan)
+- Keine Aenderung an bestehenden Interface-Initialisierungen (nur Registrierungs-Call hinzufuegen)
+
+## Betroffene Module
+
+- **Gateway:** `modules/shared/dbRegistry.py` (neu), `modules/shared/fkRegistry.py` (neu), `modules/system/databaseHealth.py` (neu), `modules/routes/routeAdminDatabaseHealth.py` (neu), `modules/datamodels/datamodelBase.py` (Model-Registry), ~20 Datamodel-Dateien (fk_target Annotationen), ~13 Interface-Dateien (DB-Registrierung)
+- **Frontend:** `AdminDatabaseHealthPage.tsx` (neu), Routing-Eintrag
+- **DB-Migration:** nein
+- **Andere Komponenten:** keine
+
+## Entscheidungen
+
+| Datum | Entscheidung | Begruendung |
+|-------|-------------|------------|
+| 2026-04-13 | Dynamische DB-Registry statt statischer Liste | Interfaces registrieren sich selbst — bei Add/Remove von DBs muss die Registry nicht manuell gepflegt werden |
+| 2026-04-13 | `fk_target` in `json_schema_extra` statt DB-Constraints als Quelle | Ermoeglicht Cross-DB Orphan Detection (z.B. Greenfield → App DB); DB-Constraints existieren nur in poweron_app |
+| 2026-04-13 | `__init_subclass__` fuer Model-Registry | Automatisch, kein manuelles Registrieren noetig; jede PowerOnModel-Subklasse ist sofort im Registry |
+| 2026-04-13 | `pg_stat_user_tables` fuer Row Counts statt `SELECT COUNT(*)` | Schnell (Katalog-Query), ausreichend genau fuer Health-Dashboard |
+| 2026-04-13 | SysAdmin-only Zugriff | Datenbank-Gesundheit ist eine System-Funktion, nicht mandantenspezifisch |
+
+## Umsetzungs-Checkliste
+
+### Phase 1: Infrastruktur (Backend)
+
+**LLM: Opus 4.6** (Architektur-Entscheidungen, Cross-File-Refactoring)
+
+- [ ] **DB-Registry** (`modules/shared/dbRegistry.py`)
+ - `registerDatabase(dbName: str, configPrefix: str = "DB")` — oeffentliche API, registriert eine DB
+ - `_getRegisteredDatabases() -> Dict[str, str]` — intern, gibt alle registrierten DBs zurueck
+ - `_getConnectorForDb(dbName: str) -> DatabaseConnector` — intern, Factory fuer Connector
+ - Thread-safe (Lock oder atomare Operationen)
+ - `registerDatabase` ohne `_` Prefix weil explizit als oeffentliche API fuer andere Module gedacht
+
+- [ ] **Selbst-Registrierung in allen Interfaces** (~13 Dateien)
+ - Jedes `interfaceDb*.py` und `interfaceFeature*.py` ruft `registerDatabase()` auf Modul-Ebene auf
+ - Kein Umbau der bestehenden Connector-Logik — nur ein zusaetzlicher Registrierungs-Call
+
+- [ ] **Model-Registry** (`modules/datamodels/datamodelBase.py`)
+ - `_MODEL_REGISTRY: Dict[str, Type[PowerOnModel]]` — automatisch via `__init_subclass__`
+ - `_getModelByTableName(tableName: str) -> Optional[Type[PowerOnModel]]` — Lookup
+
+### Phase 2: FK-Annotationen (Backend)
+
+**LLM: Composer Fast** (repetitive Annotation-Arbeit, klares Pattern)
+
+- [ ] **fk_target auf allen FK-Feldern** (~80-90 Felder in ~20 Dateien)
+ - `modules/datamodels/datamodelMembership.py` — UserMandate, FeatureAccess, Junctions
+ - `modules/datamodels/datamodelRbac.py` — Role, AccessRule
+ - `modules/datamodels/datamodelFeatures.py` — FeatureInstance
+ - `modules/datamodels/datamodelChat.py` — ChatWorkflow, ChatMessage, ChatLog, ChatDocument
+ - `modules/datamodels/datamodelFiles.py` — FileItem, FileFolder
+ - `modules/datamodels/datamodelKnowledge.py` — FileContentIndex, ContentChunk, WorkflowMemory
+ - `modules/datamodels/datamodelBilling.py` — BillingAccount, BillingTransaction, BillingSettings
+ - `modules/datamodels/datamodelSubscription.py` — MandateSubscription
+ - `modules/datamodels/datamodelInvitation.py` — Invitation
+ - `modules/datamodels/datamodelDataSource.py` — DataSource
+ - `modules/datamodels/datamodelFeatureDataSource.py` — FeatureDataSource
+ - `modules/datamodels/datamodelUam.py` — Token, AuthEvent, UserConnection, UserVoicePreferences, UserNotification
+ - `modules/features/graphicalEditor/datamodelFeatureGraphicalEditor.py` — AutoWorkflow, AutoVersion, AutoRun, AutoStepLog, AutoTask
+ - `modules/features/trustee/datamodelFeatureTrustee.py` — alle Trustee-Models
+ - `modules/features/workspace/datamodelFeatureWorkspace.py` — WorkspaceUserSettings
+ - `modules/features/neutralization/datamodelFeatureNeutralizer.py` — DataNeutraliserConfig
+ - `modules/features/realEstate/datamodelFeatureRealEstate.py` — Kanton, Parzelle, Projekt
+ - `modules/datamodels/datamodelContent.py` — ContentChunk, ContentObject (falls vorhanden)
+ - `modules/interfaces/interfaceDbManagement.py` — MessagingSubscription, Prompt (falls Models dort)
+
+### Phase 3: FK-Discovery und Orphan-Scanner (Backend)
+
+**LLM: Opus 4.6** (komplexe Cross-DB-Logik, SQL-Generierung)
+
+- [ ] **FK-Discovery** (`modules/shared/fkRegistry.py`)
+ - `_discoverFkRelationships() -> List[FkRelationship]` — scannt Model-Registry, liest `fk_target`
+ - Cached nach erstem Scan (Models aendern sich nicht zur Laufzeit)
+ - `FkRelationship` Dataclass: `sourceDb, sourceTable, sourceColumn, targetDb, targetTable`
+
+- [ ] **Orphan-Scanner** (`modules/system/databaseHealth.py`)
+ - `_scanOrphans(dbFilter: Optional[str]) -> List[OrphanResult]`
+ - Same-DB: `SELECT COUNT(*) WHERE col NOT IN (SELECT id FROM parent)`
+ - Cross-DB: Parent-IDs laden, dann `WHERE col NOT IN (...)`
+ - `_cleanOrphans(db, table, column) -> int` — loescht Orphans, gibt Count zurueck
+ - `_getTableStats(dbFilter: Optional[str]) -> List[TableStats]` — pg_stat_user_tables Query
+
+### Phase 4: API-Endpunkte (Backend)
+
+**LLM: Composer Fast** (Standard-Route-Pattern, klar definiert)
+
+- [ ] **Route** (`modules/routes/routeAdminDatabaseHealth.py`)
+ - Prefix: `/api/admin/database-health`
+ - `GET /stats` — Tabellenstatistiken (optional `?db=...`)
+ - `GET /orphans` — Orphan-Scan (optional `?db=...`)
+ - `POST /orphans/clean` — Cleanup Body: `{"db": "...", "table": "...", "column": "..."}`
+ - `POST /orphans/clean-all` — Alle Orphans bereinigen
+ - Alle Endpunkte: SysAdmin-only via `requireSysAdminRole`
+
+- [ ] **Router in app.py registrieren**
+
+### Phase 5: Navigation (Backend)
+
+**LLM: Composer Fast** (einzelne Zeile in mainSystem.py)
+
+- [ ] **Navigation-Eintrag** in `modules/system/mainSystem.py`
+ - Unter `admin-system-group`, order 98, sysAdminOnly
+ - Icon: `FaDatabase`, Path: `/admin/database-health`
+
+### Phase 6: Frontend
+
+**LLM: Opus 4.6** (neue Seite mit Tabs, FormGeneratorTable-Integration)
+
+- [ ] **AdminDatabaseHealthPage.tsx** mit zwei Tabs
+ - Tab 1 "Statistiken": FormGeneratorTable mit DB, Tabelle, Rows, Total Size, Table Size, Index Size, Last Vacuum, Last Analyze. Sortierbar, Summary-Zeile oben.
+ - Tab 2 "Orphan Cleanup": FormGeneratorTable mit DB, Tabelle, FK-Spalte, Referenz, Orphans, Total Rows, Clean-Button. Filter "Nur Probleme". Batch-Clean.
+- [ ] **Routing** in Frontend PAGE_REGISTRY
+- [ ] **i18n** — alle Labels mit `t()` taggen
+
+### Querschnitt-Checks
+
+- [ ] API-Endpunkte: 4 neue (stats, orphans, clean, clean-all)
+- [ ] DB-Schema / Migration: nein
+- [ ] Frontend-Komponenten: 1 neue Seite (2 Tabs)
+- [ ] RBAC / Permissions: SysAdmin-only
+- [ ] Neutralisierung betroffen? nein
+- [ ] Navigation / Routing: 1 neuer Eintrag
+- [ ] Billing-Impact? nein
+
+## Akzeptanzkriterien
+
+| # | Kriterium (Given-When-Then) | Prio |
+|---|---------------------------|------|
+| 1 | Given SysAdmin auf /admin/database-health, When Tab "Statistiken" geladen, Then werden alle Tabellen aller registrierten DBs mit Row Count und Size angezeigt | must |
+| 2 | Given SysAdmin auf Tab "Orphan Cleanup", When Scan ausgefuehrt, Then werden alle FK-Beziehungen mit Orphan-Count angezeigt | must |
+| 3 | Given Orphans in AutoWorkflow.featureInstanceId (Cross-DB), When "Clean" geklickt, Then werden die verwaisten AutoWorkflow-Zeilen geloescht und Count aktualisiert | must |
+| 4 | Given neue Feature-DB wird hinzugefuegt (z.B. poweron_newfeature), When Interface sich registriert und Scan laeuft, Then erscheint die neue DB automatisch in Stats und Orphan-Scan | must |
+| 5 | Given kein SysAdmin-User, When /admin/database-health aufgerufen, Then 403 Forbidden | must |
+| 6 | Given grosse Tabelle (>100k Rows), When Orphan-Scan laeuft, Then antwortet der Scan innerhalb von 30 Sekunden | should |
+| 7 | Given "Alle bereinigen" geklickt, When mehrere Tabellen Orphans haben, Then werden alle Orphans in allen Tabellen bereinigt und Summary angezeigt | should |
+
+## Testplan
+
+| ID | AC | Art | Automatisiert | Methode | Status |
+|----|----|-----|--------------|---------|--------|
+| T1 | 1 | api | nein | Manuell: GET /stats, pruefen ob alle DBs und Tabellen erscheinen | pending |
+| T2 | 2 | api | nein | Manuell: Demo laden, Demo entfernen (ohne Cascade), GET /orphans pruefen | pending |
+| T3 | 3 | api | nein | Manuell: POST /orphans/clean fuer AutoWorkflow, dann GET /orphans erneut | pending |
+| T4 | 4 | unit | nein | Manuell: _registerDatabase() aufrufen, pruefen ob in _getRegisteredDatabases() | pending |
+| T5 | 5 | api | nein | Manuell: Non-SysAdmin Request, 403 pruefen | pending |
+| T6 | 7 | e2e | nein | Manuell: Mehrere Orphan-Typen erzeugen, "Alle bereinigen", Stats pruefen | pending |
+
+## Links
+
+- Cursor-Plan: `.cursor/plans/data_cleanup_admin_page_0a610562.plan.md`
+- Demo-Config Bug-Fix: `gateway/modules/demoConfigs/investorDemo2026.py` (remove() Cascade)
+- deleteMandate Cascade-Fix: `gateway/modules/interfaces/interfaceDbApp.py` (_cascadeDeleteGraphicalEditorData)
+- Coding-Conventions: `wiki/d-guides/coding-conventions.md`
+
+## Abschluss
+
+- [ ] b-reference/ aktualisiert (gateway/architecture.md — neuer Abschnitt "Database Health")
+- [ ] TOPICS.md aktualisiert (neues Thema "Database Health / Data Cleanup")
+- [ ] Dieses Dokument → z-archive/ verschoben
diff --git a/z-archive/post-cleanup-report-2026-04-12.md b/z-archive/post-cleanup-report-2026-04-12.md
index 32d58a1..f5326e4 100644
--- a/z-archive/post-cleanup-report-2026-04-12.md
+++ b/z-archive/post-cleanup-report-2026-04-12.md
@@ -57,19 +57,23 @@ Dieser Endpunkt (`GET /user-language-options` in `routeI18n.py`) wurde entfernt.
---
-### 2.3 Billing filter-values — KEIN Handlungsbedarf
+### 2.3 Billing filter-values — Erledigt (2026-04-13)
-**Befund:** `BillingDataView.tsx` ruft `/api/billing/view/users/transactions/filter-values` auf. Dieser Endpunkt existiert weiterhin in `routeBilling.py` (Zeile 1805). Es wurde nur der **Admin**-Endpunkt `GET /admin/transactions/{targetMandateId}/filter-values` entfernt, der im Frontend nirgends referenziert war.
+**Befund (alt):** `BillingDataView.tsx` rief `/api/billing/view/users/transactions/filter-values` auf.
-**Empfehlung:** Kein Handlungsbedarf.
+**Update:** Im Rahmen der Unified Filter API Migration (2026-04-13) wurde das gesamte `/filter-values`-Pattern eliminiert. `BillingDataView.tsx` nutzt jetzt `GET /api/billing/view/users/transactions?mode=filterValues&column=xxx`. Der separate Endpunkt existiert nicht mehr.
---
-### 2.4 FormGeneratorTable — Generisches filter-values-Pattern
+### 2.4 FormGeneratorTable — Unified Filter API (aktualisiert 2026-04-13)
-**Befund:** `FormGeneratorTable.tsx` baut generisch `{apiEndpoint}/filter-values`-URLs zusammen. Das ist ein allgemeines Pattern — jede Route, die als `apiEndpoint` übergeben wird, muss einen `/filter-values`-Subpfad anbieten.
+**Befund (alt):** `FormGeneratorTable.tsx` baute generisch `{apiEndpoint}/filter-values`-URLs zusammen.
-**Empfehlung:** Kein direkter Handlungsbedarf durch die Cleanup-Aktion. Aber: Falls in Zukunft Endpunkte entfernt werden, die als `apiEndpoint` im FormGenerator genutzt werden, muss der zugehörige `filter-values`-Pfad mitberücksichtigt werden.
+**Update:** Das `/filter-values`-Sub-Pfad-Pattern wurde komplett durch Query-Parameter auf dem Haupt-Endpunkt ersetzt:
+- `GET {apiEndpoint}?mode=filterValues&column=xxx` — Distinct-Werte für Spaltenfilter
+- `GET {apiEndpoint}?mode=ids&pagination={filters}` — Alle gefilterten IDs (für "Select All Filtered")
+
+Alle separaten `/filter-values`-Endpunkte in Core-Routes und Feature-Routes (Trustee, RealEstate) wurden entfernt. Siehe `wiki/b-reference/frontend-nyla/formgenerator.md` für die aktuelle Dokumentation.
---