wiki/c-work/1-plan/2026-04-database-health-and-data-cleanup.md
ValueOn AG cf2e875968 uüd
2026-04-14 00:15:28 +02:00

228 lines
13 KiB
Markdown

<!-- status: plan -->
<!-- started: 2026-04-13 -->
<!-- component: gateway | frontend-nyla -->
# 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