232 lines
13 KiB
Markdown
232 lines
13 KiB
Markdown
<!-- status: done -->
|
|
<!-- started: 2026-04-13 -->
|
|
<!-- completed: 2026-04-16 -->
|
|
<!-- 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)
|
|
|
|
- [x] **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
|
|
|
|
- [x] **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
|
|
|
|
- [x] **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)
|
|
|
|
- [x] **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)
|
|
|
|
- [x] **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, targetColumn`
|
|
- Table-to-DB Mapping automatisch aus `fk_target`-Annotationen + Catalog-Fallback
|
|
|
|
- [x] **Orphan-Scanner** (`modules/system/databaseHealth.py`)
|
|
- `_scanOrphans(dbFilter: Optional[str]) -> List[OrphanResult]`
|
|
- Same-DB: `NOT EXISTS (SELECT 1 FROM parent WHERE parent.col = source.col)`
|
|
- Cross-DB: Parent-IDs laden, dann `NOT IN (unnest(...))`
|
|
- `_cleanOrphans(db, table, column) -> int` — loescht Orphans, gibt Count zurueck
|
|
- `_cleanAllOrphans() -> List[dict]` — alle Orphans bereinigen
|
|
- `_getTableStats(dbFilter: Optional[str]) -> List[TableStats]` — pg_stat_user_tables Query
|
|
- 5-Minuten-Cache fuer Orphan-Ergebnisse
|
|
|
|
### Phase 4: API-Endpunkte (Backend)
|
|
|
|
**LLM: Composer Fast** (Standard-Route-Pattern, klar definiert)
|
|
|
|
- [x] **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`
|
|
|
|
- [x] **Router in app.py registrieren**
|
|
|
|
### Phase 5: Navigation (Backend)
|
|
|
|
**LLM: Composer Fast** (einzelne Zeile in mainSystem.py)
|
|
|
|
- [x] **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)
|
|
|
|
- [x] **AdminDatabaseHealthPage.tsx** mit zwei Tabs
|
|
- Tab 1 "Statistiken": Sortierbare Tabelle mit DB, Tabelle, Rows, Total Size, Index Size, Last Vacuum, Last Analyze. Summary-Zeile oben. DB-Filter.
|
|
- Tab 2 "Orphan Cleanup": Tabelle mit Source DB, Tabelle, FK-Spalte, Referenz (inkl. cross-db Badge), Orphans, Clean-Button. Filter "Nur Probleme". Batch-Clean-All.
|
|
- [x] **Routing** in Frontend PAGE_REGISTRY + App.tsx Route + admin/index.ts Export
|
|
- [x] **i18n** — alle Labels mit `t()` getaggt
|
|
|
|
### 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
|
|
|
|
- [x] b-reference/ aktualisiert (platform/database-architecture.md — neuer Abschnitt "Database Health")
|
|
- [x] TOPICS.md aktualisiert (neues Thema "Database Health / Data Cleanup")
|
|
- [x] Dieses Dokument → z-archive/ verschoben
|