abbe7a18fc
- P0: hooks.py 3-tuple fix, trigger_dispatcher Contract, contacts/plugin unregister_actions_by_owner - P0: 5 test files — check_permission mocks removed, hardcoded DB credential → env var - P1: attachment_service DmsFile via Contract helper, restore_registry/history_hooks dedup - P1: mail/plugin restore unregister, mcp_client datetime.now(UTC), saved_views/filters patterns - P1: ProtectedRoute fail-closed, 13 test assertion fixes (bcrypt, DB-URLs, SECRET_KEYs) - P2: deprecated notifications → post_system_message (3 files), forgejo Base, report_generator lazy import - P2: webhooks permissions, deps.py/roles.py plugin perms removed, import_export default - P2: address/tags/entity_links patterns removed, worker.py Contract-Umgehungen fixed - P2: 28 frontend TODOs (hardcoded constants, deprecated notification API) - P3: dead code, duplicates, deprecated imports, private attr, __import__ inline - P3: 8 frontend TODOs (LucideIcons, inline styles, XSS, i18n) - ruff: 838 → 0 (612 auto-fix + 246 manual + 27 F821 regression fix) - F821: 30 → 0 (AutomationDefinition, DmsFile, user_id, Path, Any, String) - Contract-Umgehungen: 2 neue gefunden (worker.py:169, worker.py:280) und gefixt
490 lines
23 KiB
Markdown
490 lines
23 KiB
Markdown
# Architecture Cleanup Plan — LeoCRM
|
|
|
|
**Erstellt:** 2026-08-14
|
|
**Basis:** Adversarial Architecture Audit (P0-1 bis P0-10, P1-9 bis P1-24)
|
|
**Ziel:** Plugin-System funktionsfähig machen, Hartcodierungen entfernen, Core/Plugin-Grenze etablieren
|
|
|
|
---
|
|
|
|
## Prinzipien
|
|
|
|
1. **Jeder Fix nutzt existierende Interfaces** — kein Neubau, nur Verdrahtung
|
|
2. **Jeder Fix ist testbar** — Plugin aktivieren/deaktivieren ohne Neustart muss funktionieren
|
|
3. **Keine neuen Hartcodierungen** — jede neue Entität/Permission/Job kommt aus Plugin-Manifesten
|
|
4. **Minimal-invasiv** — Core-Routes bleiben statisch (nicht migrieren), nur Plugin-Teile dynamisieren
|
|
5. **Phase für Phase verifizierbar** — jede Phase hat klare Acceptance Criteria
|
|
|
|
---
|
|
|
|
## Phase 1: Plugin-Lifecycle zur Laufzeit funktionsfähig machen
|
|
|
|
**Priorität:** P0-Kritisch | **Aufwand:** ~4h | **Abhängigkeiten:** keine
|
|
|
|
### Problem
|
|
Plugin-Aktivierung/Deaktivierung zur Laufzeit funktioniert nicht (P0-10, P0-9, P0-1).
|
|
|
|
### Tasks
|
|
|
|
#### 1.1 Permission-Registry bei Runtime-Aktivierung aktualisieren (P0-10)
|
|
**Datei:** `app/services/plugin_service.py`
|
|
**Änderung:**
|
|
- In `activate_plugin()`: Nach `registry.activate()` → `register_plugin_permissions(name, plugin.manifest.permissions)` aufrufen
|
|
- In `deactivate_plugin()`: Nach `registry.deactivate()` → `unregister_plugin_permissions(name)` aufrufen
|
|
- `from app.core.permission_registry import register_plugin_permissions, unregister_plugin_permissions`
|
|
|
|
**Verifikation:**
|
|
1. Plugin via API aktivieren → Permission-Check für Plugin-Route gibt kein 403 mehr
|
|
2. Plugin via API deaktivieren → Permission-Registry enthält Plugin nicht mehr
|
|
3. Test: `test_plugin_lifecycle.py` — aktivieren, Route callen, deaktivieren, Route gibt 403
|
|
|
|
#### 1.2 Route-Registrierung aus Discovery-Ergebnis (P0-1)
|
|
**Datei:** `app/main.py:583-605`
|
|
**Änderung:**
|
|
- Ersetze hartcodierte `plugin_modules`-Liste mit `registry.list_discovered()`
|
|
- Für jeden discovered Plugin: Lese Manifest-Routes, registriere mit `require_active_plugin()`
|
|
- Behalte `try/except` für robustness
|
|
|
|
```python
|
|
# ALT:
|
|
plugin_modules = ["app.plugins.builtins.tags", ...]
|
|
|
|
# NEU:
|
|
registry = get_registry()
|
|
for plugin_name in registry.list_discovered():
|
|
plugin = registry.get_plugin(plugin_name)
|
|
if plugin and plugin.manifest.routes:
|
|
for route_def in plugin.manifest.routes:
|
|
# ... gleiche Logik wie bisher, aber dynamisch
|
|
```
|
|
|
|
**Verifikation:**
|
|
1. Neues Plugin-Verzeichnis in `app/plugins/builtins/` erstellen → Routes erscheinen ohne main.py-Änderung
|
|
2. Alle existierenden Plugin-Routes noch vorhanden (OpenAPI check)
|
|
3. `builtins/__init__.py`-Imports entfernen → Discovery findet Plugins trotzdem
|
|
|
|
#### 1.3 `_mounted_routes` befüllen oder Route-Removal dokumentieren (P0-9)
|
|
**Datei:** `app/plugins/registry.py:36,690-692`
|
|
**Änderung (Option A — empfohlen):**
|
|
- In `activate()`: Nach `app.include_router()` → `_mounted_routes[name].append(router)`
|
|
- Das erfordert, dass `activate()` Zugriff auf `app` hat (bereits via `self._app`)
|
|
- Route-Removal in `deactivate()` funktioniert dann tatsächlich
|
|
|
|
**ODER Option B — einfacher:**
|
|
- Entferne Route-Removal-Logik aus `deactivate()`
|
|
- Dokumentiere: Routes bleiben registriert, `require_active_plugin()` ist die einzige Verteidigung
|
|
- Das ist die aktuelle Realität — nur ehrlich dokumentiert
|
|
|
|
**Verifikation:**
|
|
- Option A: Plugin deaktivieren → Route gibt 404 (nicht 403)
|
|
- Option B: Plugin deaktivieren → Route gibt 403 (dokumentiert)
|
|
|
|
#### 1.4 `builtins/__init__.py`-Imports entfernen (P0-6)
|
|
**Datei:** `app/plugins/builtins/__init__.py`
|
|
**Änderung:**
|
|
- Entferne alle 10 hartcodierten Plugin-Imports
|
|
- `discover_builtins()` findet Plugins via `pkgutil` — die `__init__.py`-Imports sind redundant
|
|
- Behalte nur den Docstring
|
|
|
|
**Verifikation:**
|
|
1. App startet ohne Fehler
|
|
2. `registry.list_discovered()` enthält alle 21 Plugins
|
|
3. Alle Plugin-Routes registriert
|
|
|
|
### Acceptance Criteria Phase 1
|
|
- [ ] Plugin via API aktivieren → Routes funktionieren ohne Neustart
|
|
- [ ] Plugin via API deaktivieren → Routes geben 403/404
|
|
- [ ] Neues Plugin in `builtins/` ablegen → Routes erscheinen ohne Core-Änderung
|
|
- [ ] `builtins/__init__.py` hat keine Plugin-Imports mehr
|
|
- [ ] Test: `test_plugin_lifecycle.py` existiert und ist grün
|
|
|
|
---
|
|
|
|
## Phase 2: Plugin-Selbstregistrierung statt Core-Hartcodierung
|
|
|
|
**Priorität:** P0-Hoch | **Aufwand:** ~6h | **Abhängigkeiten:** Phase 1
|
|
|
|
### Problem
|
|
Core registriert Plugin-Entities, Hooks, Restore-Configs, Worker-Jobs hartkodiert (P0-7, P0-8, P0-5).
|
|
|
|
### Tasks
|
|
|
|
#### 2.1 Restore-Registry: Plugins registrieren selbst (P0-7)
|
|
**Dateien:**
|
|
- `app/core/restore_registry.py:113-195` — entferne `register_default_entities()` für Plugin-Entities
|
|
- `app/plugins/builtins/tasks/plugin.py` — in `on_activate()`: `reg.register(RestoreConfig(entity_type="task", ...))`
|
|
- `app/plugins/builtins/calendar/plugin.py` — in `on_activate()`: `reg.register(RestoreConfig(entity_type="calendar_entry", ...))`
|
|
- `app/plugins/builtins/dms/plugin.py` — in `on_activate()`: `reg.register(RestoreConfig(entity_type="dms_file", ...))`
|
|
- `app/plugins/builtins/mail/plugin.py` — in `on_activate()`: `reg.register(RestoreConfig(entity_type="mail", ..., special_handler=_mail_restore_handler))`
|
|
- `app/plugins/builtins/mail/plugin.py` — `_mail_restore_handler` nach Mail-Plugin verschieben
|
|
|
|
**Core behält nur:** Contact-Registrierung (Contact ist Core)
|
|
|
|
**Verifikation:**
|
|
1. Plugin aktivieren → Restore für Plugin-Entity funktioniert
|
|
2. Plugin deaktivieren → Restore-Config für Plugin-Entity entfernt
|
|
3. `register_default_entities()` registriert nur noch Contact
|
|
|
|
#### 2.2 History-Hooks: Plugins registrieren selbst (P0-8)
|
|
**Dateien:**
|
|
- `app/core/history_hooks.py:135-173` — entferne Plugin-Entity-Hooks aus `register_default_history_hooks()`
|
|
- `app/plugins/builtins/tasks/plugin.py` — in `on_activate()`: `register_history_hooks(reg, "task", "task.after_create", ...)`
|
|
- `app/plugins/builtins/calendar/plugin.py` — gleiche für `calendar_entry`
|
|
- `app/plugins/builtins/dms/plugin.py` — gleiche für `dms_file`
|
|
- `app/plugins/builtins/mail/plugin.py` — gleiche für `mail`
|
|
|
|
**Core behält nur:** Contact-Hooks
|
|
|
|
**Verifikation:**
|
|
1. Plugin aktivieren → History wird für Plugin-Entities aufgezeichnet
|
|
2. Plugin deaktivieren → Hooks werden entfernt (`on_deactivate` muss `reg.unregister_action()` aufrufen)
|
|
3. `register_default_history_hooks()` registriert nur noch Contact
|
|
|
|
#### 2.3 Worker-Jobs: Plugins registrieren selbst (P0-5)
|
|
**Dateien:**
|
|
- `app/core/worker.py:221-227` — entferne hartcodierte `plugin_job_modules`-Liste
|
|
- `app/plugins/base.py` — füge `get_job_modules() -> list[str]` hinzu (default: `[]`)
|
|
- Jedes Plugin mit Jobs: überschreibe `get_job_modules()` → return `["app.plugins.builtins.<name>.jobs"]`
|
|
- `worker.py` — iteriere `registry.list_discovered()`, rufe `plugin.get_job_modules()` auf, importiere dynamisch
|
|
|
|
**Verifikation:**
|
|
1. Plugin mit Jobs aktivieren → Jobs laufen
|
|
2. Plugin deaktivieren → Jobs werden nicht mehr geladen
|
|
3. Neues Plugin mit Jobs → funktioniert ohne worker.py-Änderung
|
|
|
|
#### 2.4 OpenAPI-Tags dynamisch aus Manifesten (P1-18)
|
|
**Datei:** `app/main.py:300-370`
|
|
**Änderung:**
|
|
- Entferne Plugin-spezifische OpenAPI-Tags (`dms`, `mail`, `calendar`, `search`, etc.)
|
|
- Behalte nur Core-Tags (`health`, `auth`, `users`, `contacts`, etc.)
|
|
- Nach Plugin-Route-Registrierung: füge Tags aus Plugin-Manifest hinzu
|
|
|
|
**Verifikation:**
|
|
1. OpenAPI-Schema enthält alle Plugin-Tags mit Beschreibungen
|
|
2. Plugin deaktivieren → Tag verschwindet aus OpenAPI
|
|
|
|
### Acceptance Criteria Phase 2
|
|
- [ ] `register_default_entities()` registriert nur Contact
|
|
- [ ] `register_default_history_hooks()` registriert nur Contact
|
|
- [ ] `worker.py` hat keine hartkodierte Plugin-Modulliste
|
|
- [ ] `main.py` OpenAPI-Tags enthalten keine Plugin-Tags mehr
|
|
- [ ] Plugin deaktivieren entfernt Restore-Config, History-Hooks, Worker-Jobs
|
|
- [ ] Plugin aktivieren registriert alles neu
|
|
|
|
---
|
|
|
|
## Phase 3: Generische Services erweiterbar machen
|
|
|
|
**Priorität:** P1-Hoch | **Aufwand:** ~8h | **Abhängigkeiten:** Phase 2
|
|
|
|
### Problem
|
|
ENTITY_MODELS, CORE_PERMISSIONS, saved_views, tags haben hartcodierte Entity-Types (P0-3, P0-4, P1-12, P1-13).
|
|
|
|
### Tasks
|
|
|
|
#### 3.1 ENTITY_MODELS: Plugin-Registrierungs-Interface (P0-3)
|
|
**Dateien:**
|
|
- `app/services/entity_permission_service.py:70-190` — entferne alle `try/except` Plugin-Import-Blöcke
|
|
- `app/plugins/base.py` — füge `get_entity_models() -> dict[str, type]` hinzu (default: `{}`)
|
|
- Jedes Plugin: überschreibe `get_entity_models()` → return `{"file": DmsFile, "folder": DmsFolder}`
|
|
- `entity_permission_service.py` — neue Funktion `register_entity_model(entity_type, model_class)`
|
|
- `main.py:lifespan()` — nach Plugin-Aktivierung: iteriere Plugins, rufe `get_entity_models()`, registriere
|
|
- `registry.activate()` — rufe `register_entity_model()` für aktive Plugins
|
|
- `registry.deactivate()` — entferne Entity-Models für deaktivierte Plugins
|
|
|
|
**Core behält:** Contact, Address, Attachment, BankAccount, Workflow, Sequence, SavedFilter, SavedView, Webhook, CustomFieldDefinition, ContactFolder, EntityAttachment, EntityHistory
|
|
|
|
**Verifikation:**
|
|
1. Plugin aktivieren → ENTITY_MODELS enthält Plugin-Entities
|
|
2. Plugin deaktivieren → ENTITY_MODELS enthält Plugin-Entities nicht mehr
|
|
3. Permission-Resolution für Plugin-Entity funktioniert
|
|
4. Neues Plugin mit neuer Entität → funktioniert ohne entity_permission_service.py-Änderung
|
|
|
|
#### 3.2 CORE_PERMISSIONS: Plugin-Permissions entfernen (P0-4)
|
|
**Datei:** `app/core/permission_registry.py:21-128`
|
|
**Änderung:**
|
|
- Entferne alle Plugin-Permissions aus `CORE_PERMISSIONS` (calendar, dms, mail, tasks, comm, automation, ai, tags, entity_links, reports, search, mcp, permissions, agents, dashboard)
|
|
- Behalte nur echte Core-Permissions: contacts, users, roles, groups, audit, settings, plugins, tenants, notifications, attachments, workflows, user_preferences, sequences, addresses, taxes, currencies, import_export, workspaces, system
|
|
- Plugin-Permissions kommen bereits via `register_plugin_permissions()` aus Manifesten — das ist die dynamische Quelle
|
|
- Entferne Kommentar "Plugin permissions (registered at startup, but also listed here for completeness)"
|
|
|
|
**Verifikation:**
|
|
1. Plugin aktivieren → Plugin-Permissions in Registry
|
|
2. Plugin deaktivieren → Plugin-Permissions nicht in Registry
|
|
3. Permission-UI zeigt nur aktive Plugin-Permissions
|
|
4. Core-Permissions weiterhin verfügbar
|
|
|
|
#### 3.3 CORE_FIELD_DEFINITIONS: Contact-Felder als Plugin oder Core-Deklaration (P1-16)
|
|
**Datei:** `app/core/permission_registry.py:135-176`
|
|
**Änderung:**
|
|
- Contact-Felddefinitionen bleiben in Core (Contact ist Core)
|
|
- User-Felddefinitionen bleiben in Core (User ist Core)
|
|
- Das ist akzeptabel — Core darf Core-Felder deklarieren
|
|
- **Kein Fix nötig** — nur Dokumentation dass dies Core-spezifisch ist
|
|
|
|
#### 3.4 saved_views/saved_filters: Entity-Types dynamisch (P1-12)
|
|
**Dateien:** `app/routes/saved_views.py:19`, `app/routes/saved_filters.py:19`
|
|
**Änderung:**
|
|
- Entferne `VALID_ENTITY_TYPES = {"contacts", "mail", "calendar", "dms"}`
|
|
- Entferne Pydantic `pattern="^(contacts|mail|calendar|dms)$"`
|
|
- Stattdessen: Validiere gegen `ENTITY_MODELS.keys()` oder eine neue `get_valid_entity_types()` Funktion
|
|
- Akzeptiere jeden String, validiere zur Laufzeit gegen registrierte Entity-Types
|
|
|
|
**Verifikation:**
|
|
1. Saved View für `task` erstellen → funktioniert
|
|
2. Saved View für `nonexistent` erstellen → 422
|
|
3. Plugin deaktivieren → Saved Views für Plugin-Entity noch abrufbar aber nicht neu erstellbar
|
|
|
|
#### 3.5 tags/entity_links: VALID_ENTITY_TYPES dynamisch (P1-13)
|
|
**Dateien:** `app/plugins/builtins/tags/routes.py:25`, `app/plugins/builtins/entity_links/routes.py:22`
|
|
**Änderung:**
|
|
- Entferne hartcodierte Sets
|
|
- Tags: Validiere gegen `ENTITY_MODELS.keys()` (jede registrierte Entität kann getaggt werden)
|
|
- Entity-Links: Validiere gegen `ENTITY_MODELS.keys()` (jede registrierte Entität kann verlinkt werden)
|
|
- Frontend `tags.ts:12` — `EntityType` dynamisch aus API laden oder als `string` deklarieren
|
|
|
|
**Verifikation:**
|
|
1. Tag für `task` erstellen → funktioniert
|
|
2. Tag für `nonexistent` → 422
|
|
3. Frontend zeigt alle verfügbaren Entity-Types an
|
|
|
|
#### 3.6 Dashboard-Counts dynamisch (P1-21)
|
|
**Datei:** `app/routes/dashboard.py:57-110`
|
|
**Änderung:**
|
|
- Behalte Contact/Company/Person als Core-Counts
|
|
- Füge Plugin-Counts-Interface hinzu: `BasePlugin.get_dashboard_counts(db, tenant_id, user_id) -> list[dict]`
|
|
- `/counts`-Endpoint iteriert aktive Plugins, sammelt Counts
|
|
- Plugins können eigene Counts beitragen (z.B. Tasks: offene Tasks, Mail: ungelesene Mails)
|
|
|
|
**Verifikation:**
|
|
1. Dashboard zeigt Plugin-Counts an
|
|
2. Plugin deaktivieren → Plugin-Counts verschwinden
|
|
|
|
### Acceptance Criteria Phase 3
|
|
- [ ] `ENTITY_MODELS` enthält keine `try/except` Plugin-Import-Blöcke mehr
|
|
- [ ] `CORE_PERMISSIONS` enthält keine Plugin-Permissions mehr
|
|
- [ ] saved_views/saved_filters akzeptieren alle registrierten Entity-Types
|
|
- [ ] tags/entity_links akzeptieren alle registrierten Entity-Types
|
|
- [ ] Dashboard-Counts enthalten Plugin-Beiträge
|
|
- [ ] Plugin deaktivieren entfernt Permissions, Entity-Models aus Registries
|
|
|
|
---
|
|
|
|
## Phase 4: Core/Plugin-Abhängigkeiten reduzieren
|
|
|
|
**Priorität:** P1-Mittel | **Aufwand:** ~6h | **Abhängigkeiten:** Phase 3
|
|
|
|
### Problem
|
|
Core-Dateien importieren direkt Plugin-Modelle (P1-9, P1-10, P1-19, P1-20).
|
|
|
|
### Tasks
|
|
|
|
#### 4.1 Core→Plugin-Imports durch Contracts ersetzen (P1-9, P1-10)
|
|
**Dateien (41 Core→Plugin-Imports):**
|
|
- `app/core/notifications.py:43` → nutze `get_contract("kommunikation")` statt direktem Import
|
|
- `app/core/restore_registry.py:132-237` → nach Phase 2.1 erledigt (Plugins registrieren selbst)
|
|
- `app/core/trigger_dispatcher.py:116,174` → nutze `get_contract("automation")`
|
|
- `app/core/worker.py:169,221-227,273` → nach Phase 2.3 erledigt (dynamische Job-Discovery)
|
|
- `app/services/entity_permission_service.py:86-187` → nach Phase 3.1 erledigt (dynamische ENTITY_MODELS)
|
|
- `app/services/attachment_service.py:25` → nutze `get_contract("dms")` für File-Modell
|
|
- `app/commands/mail_commands.py:16,36,109,147` → nutze `get_contract("mail")`
|
|
- `app/commands/calendar_commands.py:38,104,153` → nutze `get_contract("calendar")`
|
|
- `app/commands/dms_commands.py:50,130` → nutze `get_contract("dms")`
|
|
- `app/ai/llm_client.py:292,319` → nutze `get_contract("ai_assistant")`
|
|
- `app/routes/errors.py:124` → nutze `get_contract("forgejo_error_reporter")` oder mache Error-Reporting generisch
|
|
- `app/main.py:151,171` → gleiche wie errors.py
|
|
|
|
**Verifikation:**
|
|
1. `grep -rn 'from app.plugins.builtins' app/core/ app/services/ app/routes/ app/commands/ app/ai/` → 0 Treffer (außer contracts)
|
|
2. Plugin deaktivieren → Core funktioniert ohne Fehler (graceful degradation)
|
|
3. `check_cross_plugin_imports.py` erweitert auf Core-Verzeichnisse
|
|
|
|
#### 4.2 Cross-Plugin-Import-Checker auf Core erweitern (P1-20)
|
|
**Datei:** `scripts/check_cross_plugin_imports.py`
|
|
**Änderung:**
|
|
- `find_python_files()` default search_path: auch `app/core/`, `app/services/`, `app/routes/`, `app/commands/`, `app/ai/` scannen
|
|
- Neue EXEMPT_PATHS für legitime Core-Imports (z.B. `main.py` für Route-Registrierung)
|
|
- CI-Pipeline prüft nun Core→Plugin-Imports auch
|
|
|
|
**Verifikation:**
|
|
1. `python scripts/check_cross_plugin_imports.py` findet 0 Verstöße
|
|
2. CI-Pipeline grün
|
|
|
|
#### 4.3 Cross-Plugin-Imports in Plugins auf Contracts umstellen (P1-19)
|
|
**Aufwand:** Hoch (226 Imports), aber mechanisch
|
|
**Priorisierung:**
|
|
- Start mit Plugins, die am häufigsten importiert werden (kommunikation, ai_assistant, unified_search)
|
|
- Jeder `from app.plugins.builtins.<plugin>.<module> import X` → `get_contract("<plugin>")` mit None-Check
|
|
- Contracts müssen alle aktuell direkt importierten Symbole exponieren
|
|
|
|
**Verifikation:**
|
|
1. `grep -rn 'from app.plugins.builtins' app/plugins/builtins/ | grep -v contracts | grep -v __init__` → 0
|
|
2. Alle Plugin-Tests grün
|
|
3. Plugin deaktivieren → abhängige Plugins degradieren gracefully
|
|
|
|
### Acceptance Criteria Phase 4
|
|
- [ ] 0 Core→Plugin-Imports (außer contracts)
|
|
- [ ] Cross-Plugin-Checker prüft Core-Verzeichnisse
|
|
- [ ] Cross-Plugin-Imports in Plugins reduziert um >80%
|
|
- [ ] Plugin deaktivieren → keine Import-Fehler in Core oder anderen Plugins
|
|
|
|
---
|
|
|
|
## Phase 5: Test-Infrastruktur und Qualität
|
|
|
|
**Priorität:** P1-Mittel | **Aufwand:** ~4h | **Abhängigkeiten:** Phase 1-4
|
|
|
|
### Problem
|
|
Tests mocken Permissions weg, conftest importiert alle Plugins hartkodiert, keine E2E-Tests für Plugin-Lifecycle (P1-14, P1-15).
|
|
|
|
### Tasks
|
|
|
|
#### 5.1 conftest.py: Plugin-Modelle dynamisch laden (P1-14)
|
|
**Datei:** `tests/conftest.py:50-80`
|
|
**Änderung:**
|
|
- Entferne alle hartcodierten Plugin-Model-Imports
|
|
- Stattdessen: iteriere `registry.list_discovered()`, rufe `plugin.get_entity_models()` auf, importiere Modelle dynamisch
|
|
- `Base.metadata.create_all()` findet alle Tabellen weil Modelle importiert wurden
|
|
|
|
```python
|
|
# ALT: 15 hartcodierte Imports
|
|
from app.plugins.builtins.calendar.models import Calendar, CalendarEntry, ...
|
|
|
|
# NEU:
|
|
registry = get_registry()
|
|
registry.discover_builtins()
|
|
for name in registry.list_discovered():
|
|
plugin = registry.get_plugin(name)
|
|
if plugin:
|
|
models = plugin.get_entity_models()
|
|
# Import module to register models with Base.metadata
|
|
for entity_type, model_class in models.items():
|
|
# Model class is already imported via get_entity_models()
|
|
pass
|
|
```
|
|
|
|
**Verifikation:**
|
|
1. Tests laufen ohne hartcodierte Plugin-Imports
|
|
2. Plugin entfernen → Tests für Plugin laufen nicht, aber andere Tests grün
|
|
3. Neues Plugin → Tests finden Modelle automatisch
|
|
|
|
#### 5.2 Plugin-Lifecycle E2E-Test (neu)
|
|
**Datei:** `tests/test_plugin_lifecycle.py` (neu)
|
|
**Inhalt:**
|
|
1. Test-Plugin erstellen (minimal, mit Route, Permission, Entity-Model, Job)
|
|
2. Plugin aktivieren via API → Route erreichbar, Permission verfügbar, Job registriert
|
|
3. Plugin deaktivieren via API → Route gibt 403/404, Permission entfernt, Job entfernt
|
|
4. Plugin wieder aktivieren → alles wieder da
|
|
5. Plugin mit Dependency aktivieren → funktioniert nur wenn Dependency aktiv
|
|
6. Plugin mit Dependency deaktivieren → wird blockiert wenn Dependency aktiv
|
|
|
|
**Verifikation:**
|
|
1. Test ist grün
|
|
2. Test läuft ohne Mocks für Permission/Visibility/Tenant
|
|
|
|
#### 5.3 Permission-Mock-Tests umstellen (P1-15)
|
|
**Dateien:** `tests/test_graph_rag.py:42`, `test_agent_memory.py:42`, `test_marketplace.py:48`, `test_external_agent_api.py:38`
|
|
**Änderung:**
|
|
- Entferne `patch("app.core.permissions.check_permission", return_value=True)`
|
|
- Stattdessen: Test-User mit echten Permissions erstellen
|
|
- `conftest.py` hat bereits `create_test_user` mit Role → nutze echte Permissions
|
|
- Für `set_tenant_context` Mocks: nutze echte DB-Session mit Tenant-Kontext
|
|
|
|
**Verifikation:**
|
|
1. Tests laufen ohne Permission-Mocks
|
|
2. Tests testen echte Permission-Enforcement
|
|
3. Test mit unzureichenden Permissions → 403 (nicht 200)
|
|
|
|
### Acceptance Criteria Phase 5
|
|
- [ ] `conftest.py` hat keine hartcodierten Plugin-Imports
|
|
- [ ] `test_plugin_lifecycle.py` existiert und ist grün
|
|
- [ ] Keine `patch("app.core.permissions.check_permission")` mehr in Tests
|
|
- [ ] Plugin-Lifecycle E2E-Test testet echte Permission/Visibility/Tenant-Isolation
|
|
|
|
---
|
|
|
|
## Phase 6: Doppelarchitekturen auflösen
|
|
|
|
**Priorität:** P1-Niedrig | **Aufwand:** ~4h | **Abhängigkeiten:** Phase 4
|
|
|
|
### Problem
|
|
Notification-Doppelarchitektur, Dedup/Import-Export Contact-spezifisch (P1-11, P1-22, P1-23).
|
|
|
|
### Tasks
|
|
|
|
#### 6.1 Notification-Doppelarchitektur dokumentieren oder auflösen (P1-11)
|
|
**Datei:** `app/core/notifications.py`
|
|
**Änderung:**
|
|
- `create_notification()` als deprecated markieren (bereits getan)
|
|
- Frontend `NotificationDropdown` auf Communication-API umstellen
|
|
- `app/routes/notifications.py` als deprecated markieren oder auf Communication redirect
|
|
- Langfristig: `notifications`-Tabelle entfernen, alles über Communication-Plugin
|
|
|
|
**Verifikation:**
|
|
1. Frontend nutzt Communication-API für Notifications
|
|
2. `notifications`-Route gibt Deprecation-Warning
|
|
|
|
#### 6.2 Dedup-Service: Plugin-Interface oder als Contact-Service deklarieren (P1-22)
|
|
**Datei:** `app/services/dedup_service.py`
|
|
**Änderung (Option A — Plugin-Interface):**
|
|
- `BasePlugin.get_dedup_config() -> DedupConfig | None` hinzufügen
|
|
- Plugins deklarieren Dedup-Felder und Match-Logik
|
|
- Dedup-Service iteriert aktive Plugins
|
|
- **Aufwand:** Hoch — generische Dedup-Engine
|
|
|
|
**ODER Option B — ehrlich deklarieren:**
|
|
- `dedup_service.py` → `contact_dedup_service.py` umbenennen
|
|
- Dokumentieren: Dedup ist Contact-spezifisch, nicht generisch
|
|
- **Aufwand:** Klein — nur Umbenennung und Doku
|
|
|
|
**Empfehlung:** Option B — Dedup ist CRM-spezifisch, muss nicht generisch sein.
|
|
|
|
#### 6.3 Import/Export: Plugin-Interface oder als Contact-Service deklarieren (P1-23)
|
|
**Datei:** `app/services/import_export_service.py`
|
|
**Gleiche Entscheidung wie 6.2:**
|
|
- Option A: Generisches Import/Export-Interface für Plugins
|
|
- Option B: Als `contact_import_export_service.py` deklarieren
|
|
|
|
**Empfehlung:** Option B für jetzt, Option A wenn ein Plugin Import/Export braucht.
|
|
|
|
### Acceptance Criteria Phase 6
|
|
- [ ] Notification-Doppelarchitektur aufgelöst oder dokumentiert
|
|
- [ ] Dedup/Import-Export als Contact-spezifisch deklariert oder generisch gemacht
|
|
|
|
---
|
|
|
|
## Gesamtaufwand
|
|
|
|
| Phase | Aufwand | Priorität | Abhängigkeit |
|
|
|-------|---------|-----------|-------------|
|
|
| 1: Plugin-Lifecycle | ~4h | P0-Kritisch | keine |
|
|
| 2: Selbstregistrierung | ~6h | P0-Hoch | Phase 1 |
|
|
| 3: Generische Services | ~8h | P1-Hoch | Phase 2 |
|
|
| 4: Core/Plugin-Abhängigkeiten | ~6h | P1-Mittel | Phase 3 |
|
|
| 5: Test-Infrastruktur | ~4h | P1-Mittel | Phase 1-4 |
|
|
| 6: Doppelarchitekturen | ~4h | P1-Niedrig | Phase 4 |
|
|
| **Total** | **~32h** | | |
|
|
|
|
Bei 8h/Tag: **4 Arbeitstage** für alle Phasen.
|
|
Phase 1 allein: **einen halben Tag**.
|
|
|
|
---
|
|
|
|
## Risiken
|
|
|
|
1. **Phase 1 kann versteckte Abhängigkeiten aufdecken** — wenn Plugin-Aktivierung zur Laufzeit zum ersten Mal richtig getestet wird, können neue Bugs sichtbar werden
|
|
2. **Phase 3.1 (ENTITY_MODELS)** ist der komplexeste Fix — das Permission-System hängt davon ab
|
|
3. **Phase 4.3 (226 Cross-Imports)** ist mechanisch aber fehleranfällig — jeder Contract muss alle Symbole exponieren
|
|
4. **Tests können brechen** — wenn Permission-Mocks entfernt werden, können Tests failen die vorher grün waren (was gut ist, aber Aufwand bedeutet)
|
|
|
|
## Erfolgsmessung
|
|
|
|
Nach Abschluss aller Phasen:
|
|
1. **Plugin hinzufügen:** 0 Core-Dateien ändern → Plugin in `builtins/` ablegen, aktivieren
|
|
2. **Plugin deaktivieren:** Alle Routes, Permissions, Jobs, Hooks, Entity-Models entfernt
|
|
3. **Plugin entfernen:** `uninstall` → alle Spuren gelöscht
|
|
4. **Neue Entität:** Plugin deklariert Entität in Manifest → Permissions, Tags, Links, Saved Views funktionieren
|
|
5. **Cross-Plugin-Checker:** 0 Verstöße in Core und Plugins
|
|
6. **E2E-Test:** Plugin-Lifecycle-Test grün ohne Mocks
|
|
|
|
Das ist das Ziel: **Ein Plugin-System, das wirklich modular ist.**
|