13 KiB
Database Health and Data Cleanup — Admin Page
Beschreibung und Kontext
SysAdmin-Seite unter Admin > System > "Datenbank-Gesundheit" mit zwei Views:
- Tabellenstatistiken — Row Count, Size (MB), Index Size, Last Vacuum/Analyze fuer alle Tabellen in allen Datenbanken
- 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:
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_sizesind 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_tupist 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_targetAnnotationen 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)
registerDatabaseohne_Prefix weil explizit als oeffentliche API fuer andere Module gedacht
-
Selbst-Registrierung in allen Interfaces (~13 Dateien)
- Jedes
interfaceDb*.pyundinterfaceFeature*.pyruftregisterDatabase()auf Modul-Ebene auf - Kein Umbau der bestehenden Connector-Logik — nur ein zusaetzlicher Registrierungs-Call
- Jedes
-
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, Junctionsmodules/datamodels/datamodelRbac.py— Role, AccessRulemodules/datamodels/datamodelFeatures.py— FeatureInstancemodules/datamodels/datamodelChat.py— ChatWorkflow, ChatMessage, ChatLog, ChatDocumentmodules/datamodels/datamodelFiles.py— FileItem, FileFoldermodules/datamodels/datamodelKnowledge.py— FileContentIndex, ContentChunk, WorkflowMemorymodules/datamodels/datamodelBilling.py— BillingAccount, BillingTransaction, BillingSettingsmodules/datamodels/datamodelSubscription.py— MandateSubscriptionmodules/datamodels/datamodelInvitation.py— Invitationmodules/datamodels/datamodelDataSource.py— DataSourcemodules/datamodels/datamodelFeatureDataSource.py— FeatureDataSourcemodules/datamodels/datamodelUam.py— Token, AuthEvent, UserConnection, UserVoicePreferences, UserNotificationmodules/features/graphicalEditor/datamodelFeatureGraphicalEditor.py— AutoWorkflow, AutoVersion, AutoRun, AutoStepLog, AutoTaskmodules/features/trustee/datamodelFeatureTrustee.py— alle Trustee-Modelsmodules/features/workspace/datamodelFeatureWorkspace.py— WorkspaceUserSettingsmodules/features/neutralization/datamodelFeatureNeutralizer.py— DataNeutraliserConfigmodules/features/realEstate/datamodelFeatureRealEstate.py— Kanton, Parzelle, Projektmodules/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, liestfk_target- Cached nach erstem Scan (Models aendern sich nicht zur Laufzeit)
FkRelationshipDataclass: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
- Prefix:
-
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
- Unter
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