Files
leocrm/docs/architecture-cleanup-plan.md
T
Agent Zero abbe7a18fc fix(audit): P0-P3 audit fixes — 838 ruff errors → 0, 30 F821 bugs fixed, 118 files changed
- 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
2026-08-16 01:17:18 +02:00

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.**