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
This commit is contained in:
Agent Zero
2026-08-16 01:17:18 +02:00
parent 3d9b76cea4
commit abbe7a18fc
306 changed files with 5912 additions and 1827 deletions
+489
View File
@@ -0,0 +1,489 @@
# 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.**
+745
View File
@@ -0,0 +1,745 @@
# Konsolidierte Fehlerliste — LeoCRM Architektur-Audit
**Datum:** 2026-08-15
**Dateien geprüft:** 750 von 1052 (siehe docs/audit-tracker.md)
**Verbleibend:** 302 Dateien (hauptsächlich Alembic-Migrationen + Test-Dateien)
---
## Zusammenfassung
| Schwere | Backend | Frontend | Tests | Total |
|---------|---------|----------|-------|-------|
| P0 | 3 | 0 | 5 | 8 |
| P1 | 10 | 1 | 26 | 37 |
| P2 | 25 | 25 | 21 | 71 |
| P3 | 20 | 30 | 8 | 58 |
| **Total** | **58** | **56** | **60** | **174** |
---
## P0 — Runtime Crashes / Security (8)
### P0-1: hooks.py:83 — unregister() _filters 2-tuple CRASH
**Datei:** `app/core/hooks.py:83`
**Beweis:** **Beweis:**
```python
# Zeile 83: _filters nutzt 2-tuple unpacking, aber register_filter speichert 3-tuple
self._filters[hook_name] = [
(p, c) for
### P0-2: trigger_dispatcher.py:127 — AutomationDefinition nicht importiert
**Datei:** `app/core/trigger_dispatcher.py:127`
**Beweis:** **Beweis:**
```python
# Zeile 127: AutomationDefinition wird in Query verwendet, aber nie importiert
query = (
select(AutomationDefinition) # Nam
### P0-3: contacts/plugin.py:88-90 — clear_actions statt unregister_actions_by_owner
**Datei:** `app/plugins/builtins/contacts/plugin.py:88-90`
**Beweis:** **Beweis:**
```python
hook_reg.clear_actions("contact.after_create") # Entfernt Hooks anderer Plugins!
hook_reg.clear_actions("contact.after_update")
### P0-T1: test_external_agent_api.py:36-39 — check_permission mocked to True
**Datei:** `tests/test_external_agent_api.py:36-39`
**Beweis:** `@pytest.fixture(autouse=True)` `patch("app.core.permissions.check_permission", return_value=True)` alle Tests bypass RBAC
### P0-T2: test_graph_rag.py:39-43 — check_permission mocked to True
**Datei:** `tests/test_graph_rag.py:39-43`
**Beweis:** Same autouse fixture alle Tests bypass RBAC
### P0-T3: test_agent_memory.py:39-43 — check_permission mocked to True
**Datei:** `tests/test_agent_memory.py:39-43`
**Beweis:** Same autouse fixture alle Tests bypass RBAC
### P0-T4: test_marketplace.py:45-49 — check_permission mocked to True
**Datei:** `tests/test_marketplace.py:45-49`
**Beweis:** Same autouse fixture alle Tests bypass RBAC
### P0-T5: test_cross_tenant_standalone.py:33 — Hardcoded DB credential
**Datei:** `tests/test_cross_tenant_standalone.py:33`
**Beweis:** `DB_URL = "postgresql+asyncpg://crm_user:4B6X2wlfbIx-PyaG8kGutsatdLbjdBUI@crm-postgres:5432/crm_db"` Passwort im Source Code
---
## P1 — Funktionale Fehler (37)
### P1-4: attachment_service.py:48 — DmsFile type hint used but not imported
**Datei:** `app/services/attachment_service.py:48`
**Beweis:** **Beweis:**
```python
def _entity_attachment_to_dict(ea: EntityAttachment, dms_file: DmsFile | None = None) -> dict[str, Any]:
```
`DmsFile` wird als
### P1-5: restore_registry.py — register_default_entities registriert Contact
**Datei:** `app/core/restore_registry.py:113-195`
**Beweis:** **Beweis:**
`register_default_entities()` registriert Contact RestoreConfig. `ContactsPlugin.on_activate()` registriert AUCH Contact RestoreConfig. →
### P1-6: history_hooks.py — register_default_history_hooks registriert Contact ohne owner_tag
**Datei:** `app/core/history_hooks.py:140`
**Beweis:** **Beweis:**
`register_default_history_hooks()` registriert Contact hooks ohne owner_tag. `ContactsPlugin.on_activate()` registriert AUCH Contact hooks
### P1-8: mail/plugin.py — on_deactivate fehlt unregister_actions_by_owner und restore unregister
**Datei:** `app/plugins/builtins/mail/plugin.py`
**Beweis:** **Beweis:**
mail/plugin.py on_deactivate hat KEIN unregister_actions_by_owner für history hooks und KEIN unregister für restore config.
**Auswirkung:*
### P1-10: saved_views.py:62 — pattern validation hardcoded
**Datei:** `app/routes/saved_views.py:62`
**Beweis:** **Beweis:**
```python
entity_type: str | None = Query(None, pattern="^(contacts|mail|calendar|dms)$"),
```
Obwohl `_validate_entity_type()` gegen ENTI
### P1-11: saved_filters.py:62 — pattern validation hardcoded
**Datei:** `app/routes/saved_filters.py:62`
**Beweis:** **Beweis:**
```python
entity_type: str | None = Query(None, pattern="^(contacts|mail|calendar|dms)$"),
```
Gleiches Problem wie saved_views.py.
**Ausw
### P1-30: mail/plugin.py:194-200 — on_deactivate fehlt restore + history unregister
**Datei:** `app/plugins/builtins/mail/plugin.py:194-200`
**Beweis:** **Beweis:**
```python
async def on_deactivate(self, db, service_container, event_bus) -> None:
# Contract abmelden
from app.plugins.builtins.c
### P1-31: frontend/src/api/tags.ts:12 — EntityType hardcoded und inkonsistent mit Backend
**Datei:** `frontend/src/api/tags.ts:12`
**Beweis:** **Beweis:**
```typescript
export type EntityType = 'contact' | 'file' | 'calendar_entry';
```
Backend validiert dynamisch gegen ENTITY_MODELS (contact
### P1-47: mail/plugin.py:194-217 — on_deactivate FEHLT restore unregister (P1)
**Datei:** `app/plugins/builtins/mail/plugin.py:194-217`
**Beweis:** **Beweis:** on_deactivate deregistriert history hooks (Zeile 213-215) aber hat KEIN `get_restore_registry().unregister("mail")`.
**Problem:** Mail res
### P1-58: mcp_client/models.py:35-36 — Naive datetime statt UTC (P1)
**Datei:** `app/plugins/builtins/mcp_client/models.py:35-36`
**Beweis:** **Beweis:** `datetime.utcnow` (naive) statt `datetime.now(UTC)`.
### P1-F1: ProtectedRoute.tsx:22-24 — Fail-open bei leeren Permissions
**Datei:** `frontend/src/components/common/ProtectedRoute.tsx:22-24`
**Beweis:** `if (perms.length === 0) { return <>{children}</>; }` — access erlaubt wenn permissions nicht geladen
### P1-T1: test_ai_copilot.py:45-47,77-79
**Beweis:** Tests silently pass bei 403 — `assert status_code in (200, 403)` then `return`
### P1-T2: test_user_service.py:38-39,82-100
**Beweis:** Non-deterministic assertions, try/except pass
### P1-T3: test_backup_service.py:37-38,50-51,63
**Beweis:** Overly permissive assertions (4 status codes)
### P1-T4: test_mcp_server.py:62,127
**Beweis:** `assert success in (True, False)` — immer True
### P1-T5: test_saved_filters.py (entire file)
**Beweis:** Missing cross-tenant isolation + RBAC tests
### P1-T6: test_tasks.py (entire file)
**Beweis:** Missing cross-tenant isolation + RBAC tests
### P1-T7: test_dashboard.py (entire file)
**Beweis:** Missing cross-tenant isolation + RBAC tests
### P1-T8: test_custom_fields.py (entire file)
**Beweis:** Missing cross-tenant isolation + RBAC tests
### P1-T9: test_calendar.py (first 200 lines)
**Beweis:** Missing cross-tenant isolation test
### P1-T10: test_workflows.py (entire file)
**Beweis:** Missing RBAC + tenant isolation tests
### P1-T11: test_notifications.py (entire file)
**Beweis:** Missing tenant isolation + RBAC tests
### P1-T12: test_companies.py (entire file)
**Beweis:** Missing visibility filter test
### P1-T13: test_contacts.py (entire file)
**Beweis:** Missing visibility filter test
### P1-T14: test_dms.py + test_dms_coverage.py + test_dms_errors.py
**Beweis:** Missing cross-tenant isolation tests
### P1-T15: test_entity_links.py (entire file)
**Beweis:** Missing cross-tenant test
### P1-T16: test_tags.py (entire file)
**Beweis:** Missing cross-tenant + RBAC test
---
## P2 — Architekturfehler (71)
### P2-7: entity_permission_service.py:59-61 — Contact hardcoded in ENTITY_MODELS
**Datei:** `app/services/entity_permission_service.py:59-61`
**Beweis:** **Beweis:**
```python
ENTITY_MODELS: dict[str, type] = {
"contact": Contact,
"contacts": Contact,
"company": Contact,
# ...
}
```
Cont
### P2-9: hooks.py:52-53 — Type-Annotationen falsch
**Datei:** `app/core/hooks.py:52-53`
**Beweis:** **Beweis:**
```python
cls._instance._actions: dict[str, list[tuple[int, Callable]]] = defaultdict(list) # sollte tuple[int, Callable, str | None]
cls
### P2-12: saved_views.py:60 — require_permission("contacts:read") hardcoded
**Datei:** `app/routes/saved_views.py:60`
**Beweis:** **Beweis:**
```python
@router.get("", dependencies=[Depends(require_permission("contacts:read"))])
```
Saved-Views benötigen `contacts:read` Permissio
### P2-13: saved_filters.py:60 — require_permission("contacts:read") hardcoded
**Datei:** `app/routes/saved_filters.py:60`
**Beweis:** **Beweis:**
```python
@router.get("", dependencies=[Depends(require_permission("contacts:read"))])
```
Gleiches Problem wie saved_views.py.
**Auswirku
### P2-14: sensitive_data.py:83-98 — DATA_EXPOSURE_POLICY hat Contact-spezifische Felder
**Datei:** `app/core/sensitive_data.py:83-98`
**Beweis:** **Beweis:**
```python
DATA_EXPOSURE_POLICY: dict[str, dict[str, dict[str, bool]]] = {
"contact": {
"code": _EXPORT_ONLY,
"accounti
### P2-15: permission_registry.py:86-122 — CORE_FIELD_DEFINITIONS hat Contact-spezifische Felder
**Datei:** `app/core/permission_registry.py:86-122`
**Beweis:** **Beweis:**
~40 Contact-spezifische Felddefinitionen hartkodiert in Core.
**Auswirkung:** Core enthält CRM-spezifische Felddefinitionen. Neue Contact-
### P2-16: sensitive_data.py:24-48 — SENSITIVE_FIELDS hat Contact/Mail-spezifische Felder
**Datei:** `app/core/sensitive_data.py:24-48`
**Beweis:** **Beweis:**
```python
SENSITIVE_FIELDS: dict[str, set[str]] = {
"contact": {"password_hash", "smtp_password", "imap_password", ...},
"mail_acc
### P2-23: report_generator/plugin.py:9 — top-level import of jobs module
**Datei:** `app/plugins/builtins/report_generator/plugin.py:9`
**Beweis:** **Beweis:**
```python
from app.plugins.builtins.report_generator import jobs # noqa: F401
```
Top-Level-Import von jobs-Modul hat Side-Effects (regis
### P2-24: base.py:81 — unregister_all_for_plugin nutzt __self__ Heuristik
**Datei:** `app/plugins/base.py:81`
**Beweis:** **Beweis:**
```python
get_hook_registry().unregister_all_for_plugin(self.manifest.name)
```
`unregister_all_for_plugin` nutzt `callback.__self__.manif
### P2-26: deps.py:21-36 — _WRITE_PERMISSIONS hardcoded mit Plugin-Permissions
**Datei:** `app/deps.py:21-36`
**Beweis:** **Beweis:**
```python
_WRITE_PERMISSIONS = [
"contacts:write",
"contacts:create",
# ...
]
```
`contacts:write` und `contacts:create` sind
### P2-27: workflow_service.py:13 — importiert deprecated Notification model
**Datei:** `app/services/workflow_service.py:13`
**Beweis:** **Beweis:**
```python
from app.models.notification import Notification
```
Workflow-Service nutzt deprecated Notification model statt kommunikation Co
### P2-28: dashboard.py:14,61-93 — hardcoded Contact counts, kein Plugin-Beitrag möglich
**Datei:** `app/routes/dashboard.py:14,61-93`
**Beweis:** **Beweis:**
```python
from app.models.contact import Contact # Core→Contact (Plugin-Entity)
# ...
contact_query = select(func.count(Contact.id)).wher
### P2-29: import_export.py:40 — entity_type default 'companies' hardcoded
**Datei:** `app/routes/import_export.py:40`
**Beweis:** **Beweis:**
```python
entity_type: str = Form("companies"),
```
Import/Export unterstützt nur 'companies' und 'contacts' (beide Contact-Modell). Kein
### P2-32: conftest.py:41-53 — hardcoded Core-Model imports trotz dynamischer Discovery
**Datei:** `tests/conftest.py:41-53`
**Beweis:** **Beweis:**
```python
from app.models.ai_conversation import AIConversation, AIMessage # noqa: F401
from app.models.contact import Contact, ContactPe
### P2-33: roles.py:27-50 — SYSTEM_PERMISSIONS hardcoded mit Plugin-Permissions
**Datei:** `app/routes/roles.py:27-50`
**Beweis:** **Beweis:**
```python
SYSTEM_PERMISSIONS: list[dict[str, str]] = [
{"key": "contacts:read", "label": "Contacts: Read", "category": "system"},
### P2-34: users.py:15 — importiert create_notification (deprecated)
**Datei:** `app/routes/users.py:15`
**Beweis:** **Beweis:**
```python
from app.core.notifications import create_notification
```
User-Route nutzt deprecated `create_notification` statt `post_system_
### P2-35: workflows/engine.py:122-130 — erstellt Notification model direkt
**Datei:** `app/workflows/engine.py:122-130`
**Beweis:** **Beweis:**
```python
notification = Notification(
tenant_id=self.tenant_id,
user_id=uuid.UUID(user_id),
type=config.get("notification_typ
### P2-38: webhooks.py:26,49 — require_permission('automation:read/write') für Core-Webhooks
**Datei:** `app/routes/webhooks.py:26,49`
**Beweis:** **Beweis:**
```python
dependencies=[Depends(require_permission("automation:read"))]
dependencies=[Depends(require_permission("automation:write"))]
```
### P2-51: address.py:9 — Hardcoded `pattern="^contact$"` (P2)
**Datei:** `app/schemas/address.py:9` und `app/routes/addresses.py:20`
**Beweis:** **Beweis:**
```python
entity_type: str = Field(..., pattern="^contact$", description="'contact'")
```
**Problem:** Address-Modell ist generisch (entit
### P2-54: frontend routes/index.tsx — Hardcoded Plugin-Routes (P2)
**Datei:** `frontend/src/routes/index.tsx:17-70`
**Beweis:** **Beweis:** 54 hardcoded lazy-loaded page imports für Plugin-Seiten. PluginRouteRenderer (Zeile 207) ist dynamisch als catch-all, aber alle Haupt-Rout
### P2-55: tags/schemas.py:27,33,39 — Hardcoded entity_type pattern (P2)
**Datei:** `app/plugins/builtins/tags/schemas.py:27,33,39`
**Beweis:** **Beweis:** `pattern="^(contact|file|folder)$"` in TagAssignRequest, TagUnassignRequest, TagBulkAssignRequest.
**Problem:** Schema limitiert auf 3 Ent
### P2-56: entity_links/schemas.py:9 — Hardcoded entity_type pattern (P2)
**Datei:** `app/plugins/builtins/entity_links/schemas.py:9`
**Beweis:** **Beweis:** `entity_type: str = Field(..., pattern="^(contact|company)$")`
**Problem:** Schema limitiert auf 2 Entity-Types, Backend validiert dynamis
### P2-57: forgejo_error_reporter/models.py:13 — Eigenes Base, nicht app.core.db.Base (P2)
**Datei:** `app/plugins/builtins/forgejo_error_reporter/models.py:13`
**Beweis:** **Beweis:**
```python
from sqlalchemy.orm import declarative_base
Base = declarative_base() # Eigenes Base, nicht app.core.db.Base!
```
**Problem:**
### P2-F1: frontend/src/routes/index.tsx:17-70
**Beweis:** 54 hardcoded lazy-loaded Plugin-Routes
### P2-F2: frontend/src/pages/Settings.tsx:18-29
**Beweis:** 10 hardcoded settings nav items
### P2-F3: frontend/src/api/search.ts:40-47
**Beweis:** ENTITY_URL_MAP hardcoded mit 6 Entity-Types
### P2-F4: frontend/src/components/search/CommandPalette.tsx:12-39
**Beweis:** TYPE_LABELS + TYPE_ICON_CLASSES + typeIcon() hardcoded
### P2-F5: frontend/src/pages/GlobalSearchResults.tsx:15-22
**Beweis:** TYPE_LABELS hardcoded (dupliziert)
### P2-F6: frontend/src/api/tags.ts:12
**Beweis:** EntityType hardcoded, inkonsistent mit Backend
### P2-F7: frontend/src/components/notifications/NotificationDropdown.tsx
**Beweis:** Nutzt alte /notifications API statt kommunikation
### P2-F8: frontend/src/components/layout/NotificationBell.tsx:13
**Beweis:** Nutzt useUnreadNotificationCount von /notifications
### P2-F9: frontend/src/api/contacts.ts + unifiedContacts.ts
**Beweis:** Dual API client (legacy + neu)
### P2-F10: frontend/src/components/contacts/FilterPanel.tsx:22-80
**Beweis:** FIELD_DEFS hardcoded ~40 Contact-Felder
### P2-F11: frontend/src/components/contacts/SortPanel.tsx:22-69
**Beweis:** SORT_FIELDS hardcoded (dupliziert)
### P2-F12: frontend/src/components/contacts/GroupPanel.tsx:22-68
**Beweis:** GROUP_FIELDS hardcoded (dupliziert, triple-dup)
### P2-F13: frontend/src/components/mail/MailFolderTree.tsx:22-43
**Beweis:** FOLDER_NAME_MAP hardcoded 20 IMAP names
### P2-F14: frontend/src/components/mail/MailFilterPanel.tsx:22-44
**Beweis:** FIELD_DEFS hardcoded 10 Mail-Felder
### P2-F15: frontend/src/components/mail/MailSortPanel.tsx:19-27
**Beweis:** SORT_FIELDS hardcoded (dupliziert)
### P2-F16: frontend/src/components/mail/MailGroupPanel.tsx:19-29
**Beweis:** GROUP_FIELDS hardcoded (dupliziert, triple-dup)
### P2-F17: frontend/src/components/dashboard/DashboardWidgetLoader.tsx:11-21
**Beweis:** widgetRegistry hardcoded 3 Widgets
### P2-F18: frontend/src/components/contacts/ContactList.tsx:28-80
**Beweis:** ALL_COLUMNS hardcoded
### P2-F19: frontend/src/api/types.ts:12-22
**Beweis:** Contact interface legacy (first_name/last_name)
### P2-F20: frontend/src/pages/SettingsWebhooks.tsx:38-56
**Beweis:** AVAILABLE_EVENTS hardcoded, 6 Events existieren nicht (deal.*, note.*)
### P2-F21: frontend/src/components/workflows/WorkflowEditor.tsx:16-27
**Beweis:** triggerEventOptions hardcoded, 4 deal.* Events existieren nicht
### P2-F22: frontend/src/pages/SettingsUsers.tsx:19-25
**Beweis:** LEGACY_ROLES hardcoded 5 Rollen
### P2-F23: frontend/src/pages/SettingsMenuOrder.tsx:34-46
**Beweis:** DEFAULT_ORDER hardcoded 11 Items
### P2-F24: frontend/src/pages/Trash.tsx:17
**Beweis:** ENTITY_TYPES hardcoded 5 Types
### P2-F25: frontend/src/pages/AgentDashboard.tsx:50-61
**Beweis:** commonModels hardcoded 10 Modellnamen
### P2-T1: test_cross_tenant_security.py:98
**Beweis:** Invalid bcrypt hash `$2b$12$testhash`
### P2-T2: test_cross_tenant_security_v2.py:132
**Beweis:** Same invalid hash
### P2-T3: test_cross_tenant_standalone.py:73
**Beweis:** Same invalid hash
### P2-T4: test_workspaces.py:42, test_api_tokens.py:33
**Beweis:** `password_hash="dummy"` — not valid bcrypt
### P2-T5: test_tags.py:138,165,194
**Beweis:** Random UUIDs für non-existent entities
### P2-T6: test_ai_proactive.py:146-150
**Beweis:** Test grants is_system_admin to bypass permissions
### P2-T7: test_commands.py:140-148
**Beweis:** Wildcard permissions `*: *` bypass real RBAC
### P2-T8: test_mcp_client.py:41
**Beweis:** API token returned in plaintext in response
### P2-T9: test_cross_tenant_security.py:40
**Beweis:** Hardcoded DB URL with default password
### P2-T10: test_cross_tenant_security_v2.py:47-56
**Beweis:** Hardcoded DB URLs with credentials
### P2-T11: test_cross_tenant_security_v2.py:34-37
**Beweis:** Hardcoded SECRET_KEY
### P2-T12: test_cross_tenant_standalone.py:18-21
**Beweis:** Hardcoded SECRET_KEY
### P2-T13: test_no_legacy_tenant_var.py:17-20
**Beweis:** Hardcoded SECRET_KEY
### P2-T14: test_entity_links.py:105
**Beweis:** Variable named contact_id but used as company entity
### P2-T15: test_external_agent_api.py:92-101
**Beweis:** Manually constructed user with is_system_admin: True
### P2-T16: test_graph_rag.py (similar)
**Beweis:** Same manually constructed user
### P2-T17: test_agent_memory.py (similar)
**Beweis:** Same manually constructed user
### P2-T18: test_marketplace.py (similar)
**Beweis:** Same manually constructed user
### P2-T19: frontend/src/pages/SettingsRechte.tsx:37-75
**Beweis:** PermissionLevelBadge + PrincipalTypeBadge hardcoded
### P2-T20: frontend/src/pages/ProactiveAISettings.tsx:3-17
**Beweis:** categoryLabels + modelOptions hardcoded
### P2-T21: frontend/src/pages/CustomFields.tsx:40-43
**Beweis:** ENTITY_OPTIONS hardcoded (contact, company)
---
## P3 — Dead Code / Code Quality / Deprecated (58)
### P3-19: registry.py:36 — _mounted_routes ist Dead Code
**Datei:** `app/plugins/registry.py:36`
**Beweis:** **Beweis:**
`self._mounted_routes: dict[str, list[Any]] = {}` wird initialisiert aber nie befüllt. Route-Removal-Logik wurde entfernt (Gate-Modell dok
### P3-20: ai_assistant/plugin.py:96-98 — direkter Import von kommunikation.contracts
**Datei:** `app/plugins/builtins/ai_assistant/plugin.py:96-98`
**Beweis:** **Beweis:**
```python
from app.plugins.builtins.kommunikation.contracts import (
get_participant_registry,
)
```
**Bewertung:** Deklariert in `dep
### P3-21: system_notif/plugin.py:161 — direkter Import von kommunikation.contracts
**Datei:** `app/plugins/builtins/system_notif/plugin.py:161`
**Beweis:** **Beweis:**
```python
from app.plugins.builtins.kommunikation.contracts import create_plugin_room, send_message
```
**Bewertung:** Deklariert in `depe
### P3-22: graph_rag/plugin.py:44,57 — direkter Import von unified_search.contracts
**Datei:** `app/plugins/builtins/graph_rag/plugin.py:44,57`
**Beweis:** **Beweis:**
```python
from app.plugins.builtins.unified_search.contracts import get_search_registry
```
**Bewertung:** Deklariert in `dependencies=["u
### P3-25: entity_permission_service.py:30 — importiert create_notification
**Datei:** `app/services/entity_permission_service.py:30`
**Beweis:** **Beweis:**
```python
from app.core.notifications import create_notification
```
`create_notification` ist deprecated und nutzt jetzt `get_contract("k
### P3-36: schemas/common.py:22-58 — Notification schemas für deprecated system
**Datei:** `app/schemas/common.py:22-58`
**Beweis:** **Beweis:**
```python
class NotificationResponse(BaseModel): ...
class NotificationListResponse(BaseModel): ...
class UnreadCountResponse(BaseModel):
### P3-37: custom_fields.py:42 — greift auf registry._plugins direkt zu
**Datei:** `app/routes/custom_fields.py:42`
**Beweis:** **Beweis:**
```python
for plugin in registry._plugins.values():
```
Greift auf privates Attribut `_plugins` zu statt öffentlichen API (`list_discovere
### P3-48: entity_attachment.py:45-49 — __import__ inline statt func import (P3)
**Datei:** `app/models/entity_attachment.py:45-49`
**Beweis:** **Beweis:**
```python
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=__import__('sqlalchemy
### P3-49: ai_copilot_service.py:18-19 — Duplicate Contact import (P3)
**Datei:** `app/services/ai_copilot_service.py:18-19`
**Beweis:** **Beweis:**
```python
from app.models.contact import Contact
from app.models.contact import Contact # Duplicate!
```
### P3-50: system_settings.py:44-54 — Duplicate field definitions (P3)
**Datei:** `app/schemas/system_settings.py:44-54`
**Beweis:** **Beweis:**
```python
class SystemSettingsResponse(BaseModel):
# Zeilen 45-48:
tax_number: str | None = None
vat_id: str | None = None
### P3-52: unified_search hardcoded entity maps (P2)
**Dateien:** `app/plugins/builtins/unified_search/search_engine.py:19-24`, `lifecycle.py:22-27`, `jobs.py:16-21`
**Beweis:** Drei separate hardcoded Entity-Maps für dieselben 4 Entity-Types (contact, mail, file, event). Duplikation, nicht erweiterbar.
**Schwere:** P2 — Architekturfehler, dreifache Duplikation.
### FEHLER 53: ai_proactive/services.py:26-27 — Duplicate Contact import (P3)
**Datei:** `app/plugins/builtins/ai_proactive/services.py:26-27`
**Beweis:** **Beweis:**
```python
from app.models.contact import Contact, ContactPerson
from app.models.contact import Contact, ContactPerson # Duplicate!
```
### P3-F1: frontend/src/pages/Settings.tsx:5
**Beweis:** `import * as LucideIcons` lädt ALLE Icons
### P3-F2: frontend/src/pages/Settings.tsx:38
**Beweis:** `(LucideIcons as any)[p.icon]` — any type cast
### P3-F3: frontend/src/components/contacts/ContactDetail.tsx:11
**Beweis:** `import * as LucideIcons` lädt ALLE Icons
### P3-F4: frontend/src/components/contacts/ContactDetail.tsx:67-74
**Beweis:** 8x `(contact as any)` type casts
### P3-F5: frontend/src/components/ai/ChatWindow.tsx:39-41
**Beweis:** inline styles für animationDelay
### P3-F6: frontend/src/components/mail/MailList.tsx:62
**Beweis:** inline style paddingLeft
### P3-F7: frontend/src/components/mail/MailDetail.tsx:49-52
**Beweis:** iframe HTML rendering (potential XSS)
### P3-F8: frontend/src/components/contacts/CustomFieldRenderer.tsx:15-16
**Beweis:** `any` types
### P3-F9: frontend/src/components/SavedFilters.tsx:22,47,56
**Beweis:** `any` types
### P3-F10: frontend/src/store/windowStore.ts:8-9
**Beweis:** `ComponentType<any>` und `Record<string, any>`
### P3-F11: frontend/src/store/commStore.ts:27,61
**Beweis:** `Record<string, any>` und `reactions: any[]`
### P3-F12: frontend/src/hooks/useCommWebSocket.ts:31
**Beweis:** `console.log` statt strukturiertem Logger
### P3-F13: frontend/src/hooks/useAIContext.ts:5
**Beweis:** `entityData?: any`
### P3-F14: frontend/src/api/auth.ts:28,36,60
**Beweis:** `any` type casts
### P3-F15: frontend/src/pages/Login.tsx:41
**Beweis:** `error: any`
### P3-F16: frontend/src/pages/Workflows.tsx:59,70
**Beweis:** `err: any`
### P3-F17: frontend/src/pages/AuditLog.tsx:37
**Beweis:** `ColumnDef<AuditLogEntry, any>`
### P3-F18: frontend/src/components/comm/blocks/BlockRenderer.tsx:59
**Beweis:** hardcoded German string
### P3-F19: frontend/src/components/comm/blocks/MiniAppBlock.tsx:13,24
**Beweis:** hardcoded German strings
### P3-F20: frontend/src/components/comm/blocks/ActionCardBlock.tsx:59
**Beweis:** hardcoded German string
### P3-F21: frontend/src/components/comm/blocks/HtmlBlock.tsx:17-20
**Beweis:** redundante regex vor DOMPurify
### P3-F22: frontend/src/components/shared/CsvImportDialog.tsx:48,71,78
**Beweis:** hardcoded German strings
### P3-F23: frontend/src/components/shared/DataGrid.tsx:20,37
**Beweis:** `any` types
### P3-F24: frontend/src/components/dms/FileDetails.tsx:73
**Beweis:** inline style DOM-Manipulation
### P3-F25: frontend/src/pages/NoAccessPage.tsx:9-13
**Beweis:** hardcoded German strings ohne i18n
### P3-F26: frontend/src/pages/ActivityTimeline.tsx:25-26
**Beweis:** hardcoded German strings
### P3-F27: frontend/src/pages/SettingsBackup.tsx:38
**Beweis:** hardcoded de-DE locale
### P3-F28: frontend/src/pages/PasswordResetConfirm.tsx:15
**Beweis:** hardcoded English validation message
### P3-F29: frontend/src/pages/DmsTrash.tsx:27-34
**Beweis:** leere Implementation (Trash-Endpoint fehlt)
### P3-F30: frontend/src/pages/GuestLogin.tsx:16 + GuestContacts.tsx:16
**Beweis:** hardcoded German strings
### P3-T1: test_agent_subtasks.py:29-31,35-37
**Beweis:** Duplicate fixture definition
### P3-T2: test_cross_tenant_security.py:187-188
**Beweis:** Duplicate @pytest.mark.asyncio
### P3-T3: test_tenant.py:189
**Beweis:** Potential KeyError (custom_role)
### P3-T4: test_api_documentation.py:10
**Beweis:** sys.path.insert path manipulation
### P3-T5: test_backup_restore.py:15
**Beweis:** sys.path.insert path manipulation
### P3-T6: test_ai_deploy.py:13
**Beweis:** sys.path.insert path manipulation
### P3-T7: test_ai_health_check.py:13
**Beweis:** sys.path.insert path manipulation
### P3-T8: test_dms_coverage.py:13
**Beweis:** Unused import
---
## Root Causes (4)
1. **Keine definierte Core/Plugin-Grenze** — Core importiert Plugin-Modelle, -Permissions, -Felder direkt
2. **Discovery existiert, wird aber umgangen** — Schema-Patterns, roles.py SYSTEM_PERMISSIONS, frontend routes
3. **Generische Services haben kein Registrierungs-Interface** — ENTITY_MODELS, restore, history, saved_views, tags, dashboard, dedup, import/export
4. **Contract-System wird umgangen** — 14 deprecated notifications, direkte Plugin→Plugin Imports, frontend nutzt alte API
---
## Verbleibende 302 Dateien
- ~100 Alembic-Migrationen (23 stichprobenartig geprüft, alle sauber)
- ~80 Frontend Test-Dateien (Vitest __tests__/)
- ~55 Backend Test-Dateien (vom Test-Audit-Subordinate abgedeckt — 60 Fehler gefunden)
- ~30 Frontend API-Clients und Hooks (bereits geprüft)
- ~20 Frontend Stores und Utils (bereits geprüft)
- ~10 Scripts (8 geprüft)
- ~7 Frontend E2E Tests
Siehe `docs/audit-tracker.md` für die vollständige Liste der geprüften und verbleibenden Dateien.
+235
View File
@@ -0,0 +1,235 @@
# Audit Fix-Plan — LeoCRM Architektur-Audit
**Basis:** `docs/audit-consolidated-errors.md` (153 eindeutige Fehler)
**Datum:** 2026-08-15
---
## Prinzipien
1. **P0 zuerst** — Runtime Crashes müssen sofort behoben werden
2. **Kein neues Architekturmodell** — vorhandene Interfaces nutzen
3. **Deklarierte Plugin→Plugin-Abhängigkeiten sind erlaubt**
4. **Minimal focused changes** — bestehenden Style beibehalten
5. **Jeder Fix wird verifiziert** — Tests laufen, App startet
---
## Phase 1: P0 Runtime Crashes (8 Fixes, ~2h)
### 1.1 hooks.py:83 — unregister() _filters 2-tuple CRASH
**Datei:** `app/core/hooks.py:83`
**Änderung:** `register_filter` speichert 3-tuple `(priority, callback, owner_tag)`, aber `unregister()` unpackt nur 2-tuple. Fix: `unregister()` auf 3-tuple unpacking aktualisieren.
**Verifikation:** `pytest tests/test_hooks.py -v`
### 1.2 trigger_dispatcher.py:127 — AutomationDefinition nicht importiert
**Datei:** `app/core/trigger_dispatcher.py:127`
**Änderung:** `AutomationDefinition` wird in Query verwendet aber nie importiert. Fix: Import hinzufügen (via Contract oder lazy import).
**Verifikation:** `pytest tests/test_trigger_core.py -v`
### 1.3 contacts/plugin.py:88-90 — clear_actions() statt unregister_actions_by_owner()
**Datei:** `app/plugins/builtins/contacts/plugin.py:88-90`
**Änderung:** `clear_actions()` entfernt Hooks ALLER Plugins. Fix: `unregister_actions_by_owner("contacts")` verwenden.
**Verifikation:** `pytest tests/test_contacts_lifecycle.py -v`
### 1.4-1.8 Test P0s — check_permission mocked + hardcoded DB credential
**Dateien:** `tests/test_external_agent_api.py`, `tests/test_graph_rag.py`, `tests/test_agent_memory.py`, `tests/test_marketplace.py`, `tests/test_cross_tenant_standalone.py`
**Änderung:**
- 4 Dateien: `patch("check_permission", return_value=True)` entfernen, echte Permission-Setup verwenden
- 1 Datei: Hardcoded DB credential durch env var ersetzen
**Verifikation:** `pytest tests/test_external_agent_api.py tests/test_graph_rag.py tests/test_agent_memory.py tests/test_marketplace.py tests/test_cross_tenant_standalone.py -v`
---
## Phase 2: P1 Funktionale Fehler (27 Fixes, ~6h)
### 2.1 Backend P1s (10 Fixes)
| # | Datei | Problem | Fix |
|---|-------|---------|-----|
| 1 | `app/services/attachment_service.py:48` | DmsFile type hint not imported | Import via Contract |
| 2 | `app/core/restore_registry.py:113-195` | register_default_entities noch Plugin-Entities | Nur Contact registrieren |
| 3 | `app/core/history_hooks.py:140` | register_default_history_hooks noch Contact | Nur Contact registrieren |
| 4 | `app/plugins/builtins/mail/plugin.py` | on_deactivate fehlt unregister_actions_by_owner | Hinzufügen |
| 5 | `app/routes/saved_views.py:62` | pattern validation hardcoded | Gegen ENTITY_MODELS validieren |
| 6 | `app/routes/saved_filters.py:62` | pattern validation hardcoded | Gegen ENTITY_MODELS validieren |
| 7 | `app/plugins/builtins/mail/plugin.py:194-200` | on_deactivate fehlt restore + history unregister | Hinzufügen |
| 8 | `app/plugins/builtins/mcp_client/models.py:35-36` | datetime.utcnow (naive) | datetime.now(UTC) |
| 9 | `frontend/src/api/tags.ts:12` | EntityType hardcoded, inkonsistent | Dynamisch aus Backend holen |
| 10 | `frontend/src/components/common/ProtectedRoute.tsx:22-24` | Fail-open bei leeren permissions | Block access bis permissions geladen |
### 2.2 Test P1s (26 Fixes)
| # | Datei(en) | Problem | Fix |
|---|----------|---------|-----|
| 1-2 | `test_ai_copilot.py:45-47,77-79` | Tests silently pass bei 403 | Exakte status codes asserten |
| 3-4 | `test_user_service.py:38-39,82-100` | Non-deterministic, try/except pass | Exakte assertions, echte Fehler testen |
| 5-7 | `test_backup_service.py:37-38,50-51,63` | Overly permissive assertions | Exakte status codes |
| 8-9 | `test_mcp_server.py:62,127` | `assert success in (True, False)` | Exakte assertions |
| 10 | `test_saved_filters.py` | Missing cross-tenant isolation + RBAC | Tests hinzufügen |
| 11 | `test_tasks.py` | Missing cross-tenant isolation + RBAC | Tests hinzufügen |
| 12 | `test_dashboard.py` | Missing cross-tenant isolation + RBAC | Tests hinzufügen |
| 13 | `test_custom_fields.py` | Missing cross-tenant isolation + RBAC | Tests hinzufügen |
| 14 | `test_calendar.py` | Missing cross-tenant isolation test | Test hinzufügen |
| 15 | `test_workflows.py` | Missing RBAC + tenant isolation tests | Tests hinzufügen |
| 16 | `test_notifications.py` | Missing tenant isolation + RBAC tests | Tests hinzufügen |
| 17 | `test_companies.py` | Missing visibility filter test | Test hinzufügen |
| 18 | `test_contacts.py` | Missing visibility filter test | Test hinzufügen |
| 19 | `test_dms.py + test_dms_coverage.py + test_dms_errors.py` | Missing cross-tenant isolation tests | Tests hinzufügen |
| 20 | `test_entity_links.py` | Missing cross-tenant test | Test hinzufügen |
| 21 | `test_tags.py` | Missing cross-tenant + RBAC test | Test hinzufügen |
| 22 | `mail/plugin.py:194-217` (P1-47) | on_deactivate FEHLT restore unregister | `get_restore_registry().unregister("mail")` hinzufügen |
| 23 | `mcp_client/models.py:35-36` (P1-58) | Naive datetime `utcnow` | `datetime.now(UTC)` |
| 24 | `frontend/src/api/tags.ts:12` (P1-31) | EntityType hardcoded, inkonsistent | Dynamisch aus Backend holen |
| 25 | `frontend/src/components/common/ProtectedRoute.tsx:22-24` (P1-F1) | Fail-open bei leeren permissions | Block access bis permissions geladen |
| 26 | `mail/plugin.py:194-200` (P1-30) | on_deactivate fehlt restore + history unregister | Hinzufügen |
---
## Phase 3: P2 Architekturfehler (69 Fixes, ~12h)
### 3.1 Backend P2s (23 Fixes)
**Core/Plugin-Grenze (8 Fixes):**
- `entity_permission_service.py:59-61` — Contact hardcoded in ENTITY_MODELS → über ContactsPlugin registrieren
- `sensitive_data.py:83-98` — DATA_EXPOSURE_POLICY Contact-spezifisch → Plugin deklarierbar
- `sensitive_data.py:24-48` — SENSITIVE_FIELDS Contact/Mail-spezifisch → Plugin deklarierbar
- `permission_registry.py:86-122` — CORE_FIELD_DEFINITIONS Contact-spezifisch → Plugin deklarierbar
- `deps.py:21-36` — _WRITE_PERMISSIONS hardcoded Plugin-Perms → Nur Core-Perms
- `roles.py:27-50` — SYSTEM_PERMISSIONS hardcoded Plugin-Perms → Dynamisch aus Registry
- `routes/webhooks.py:26,49` — Falsche permissions (automation statt webhooks) → Korrigieren
- `routes/dashboard.py:14,61-93` — Hardcoded Contact counts → Plugin-contributable
**Generische Services (5 Fixes):**
- `routes/import_export.py:40` — entity_type default 'companies' hardcoded → Dynamisch
- `schemas/address.py:9` — pattern="^contact$" hardcoded → Dynamisch
- `tags/schemas.py:27,33,39` — entity_type pattern hardcoded → Dynamisch
- `entity_links/schemas.py:9` — entity_type pattern hardcoded → Dynamisch
- `unified_search` — 3 separate hardcoded entity maps → Eine Registry
**Deprecated Notifications (3 Fixes):**
- `workflow_service.py:13` — importiert deprecated Notification → post_system_message
- `routes/users.py:15` — importiert create_notification → post_system_message
- `workflows/engine.py:122-130` — erstellt Notification model direkt → post_system_message
**Plugin Lifecycle (4 Fixes):**
- `restore_registry.py` — register_default_entities veraltet → Entfernen oder dokumentieren
- `history_hooks.py` — register_default_history_hooks veraltet → Entfernen oder dokumentieren
- `base.py:81` — unregister_all_for_plugin nutzt __self__ Heuristik → owner_tag nutzen
- `report_generator/plugin.py:9` — top-level import of jobs module → lazy import in on_activate
**Architektur (5 Fixes):**
- `forgejo_error_reporter/models.py:13` — Eigenes Base statt app.core.db.Base → app.core.db.Base nutzen
- `saved_views.py:60` — require_permission("contacts:read") hardcoded → Dynamisch
- `saved_filters.py:60` — require_permission("contacts:read") hardcoded → Dynamisch
- `worker.py:169` — direkter Import `unified_search.provider_registry.auto_register_providers``auto_register_providers` im UnifiedSearchContract exponieren und via Contract nutzen
- `worker.py:280` — direkter Import `forgejo_error_reporter.service.report_error_to_forgejo` → ForgejoErrorReporterContract nutzen (wie main.py/errors.py)
### 3.2 Frontend P2s (25 Fixes)
**Hardcoded Entity-Types (8 Fixes):**
- `routes/index.tsx:17-70` — 54 hardcoded Plugin-Routes → Dynamisch aus Manifesten
- `search.ts:40-47` — ENTITY_URL_MAP hardcoded → Dynamisch aus Backend
- `CommandPalette.tsx:12-39` — TYPE_LABELS/ICONS hardcoded → Dynamisch
- `GlobalSearchResults.tsx:15-22` — TYPE_LABELS hardcoded (dupliziert) → Gemeinsame Konstante
- `tags.ts:12` — EntityType hardcoded → Dynamisch aus Backend
- `Trash.tsx:17` — ENTITY_TYPES hardcoded → Dynamisch
- `CustomFields.tsx:40-43` — ENTITY_OPTIONS hardcoded → Dynamisch
- `ImportWizard.tsx:45-48 + ExportPanel.tsx:13-16` — ENTITY_OPTIONS dupliziert → Gemeinsame Konstante
**Hardcoded Field Definitions (6 Fixes):**
- `FilterPanel.tsx:22-80` — FIELD_DEFS hardcoded → Aus Backend/Manifest holen
- `SortPanel.tsx:22-69` — SORT_FIELDS hardcoded (dupliziert) → Gemeinsame Konstante
- `GroupPanel.tsx:22-68` — GROUP_FIELDS hardcoded (dupliziert) → Gemeinsame Konstante
- `MailFilterPanel.tsx:22-44` — FIELD_DEFS hardcoded → Aus Backend holen
- `MailSortPanel.tsx:19-27` — SORT_FIELDS hardcoded (dupliziert) → Gemeinsame Konstante
- `MailGroupPanel.tsx:19-29` — GROUP_FIELDS hardcoded (dupliziert) → Gemeinsame Konstante
**Deprecated Notification API (2 Fixes):**
- `NotificationDropdown.tsx` — Nutzt alte /notifications API → Communication API
- `NotificationBell.tsx:13` — Nutzt useUnreadNotificationCount von /notifications → Communication API
**Hardcoded Options (9 Fixes):**
- `Settings.tsx:18-29` — 10 hardcoded settings nav items → Dynamisch
- `SettingsWebhooks.tsx:38-56` — AVAILABLE_EVENTS hardcoded, 6 nicht existent → Aus Backend holen
- `WorkflowEditor.tsx:16-27` — triggerEventOptions hardcoded, 4 nicht existent → Aus Backend holen
- `SettingsUsers.tsx:19-25` — LEGACY_ROLES hardcoded → Aus /roles API holen
- `SettingsMenuOrder.tsx:34-46` — DEFAULT_ORDER hardcoded → Aus Backend holen
- `AgentDashboard.tsx:50-61` — commonModels hardcoded → Aus /ai/providers API holen
- `DashboardWidgetLoader.tsx:11-21` — widgetRegistry hardcoded → Dynamisch aus Manifesten
- `ContactList.tsx:28-80` — ALL_COLUMNS hardcoded → Aus Backend/Manifest holen
- `MailFolderTree.tsx:22-43` — FOLDER_NAME_MAP hardcoded → i18n keys
### 3.3 Test P2s (21 Fixes)
- 3x Invalid bcrypt hash `$2b$12$testhash``hash_password("TestPass123!")` (`test_cross_tenant_security.py:98`, `test_cross_tenant_security_v2.py:132`, `test_cross_tenant_standalone.py:73`)
- 2x `password_hash="dummy"` → Proper bcrypt hash (`test_workspaces.py:42`, `test_api_tokens.py:33`)
- 3x Random UUIDs für non-existent entities → Echte Entity-IDs aus DB (`test_tags.py:138,165,194`)
- 1x is_system_admin bypass → Echte Permission-Setup (`test_ai_proactive.py:146-150`)
- 1x Wildcard permissions `*: *` → Echte Permissions (`test_commands.py:140-148`)
- 1x API token in plaintext → Token nicht in Response asserten (`test_mcp_client.py:41`)
- 3x Hardcoded DB URLs → env vars (`test_cross_tenant_security.py:40`, `test_cross_tenant_security_v2.py:47-56`, `test_cross_tenant_standalone.py:33`)
- 3x Hardcoded SECRET_KEY → env var/conftest (`test_cross_tenant_security_v2.py:34-37`, `test_cross_tenant_standalone.py:18-21`, `test_no_legacy_tenant_var.py:17-20`)
- 1x Variable naming mismatch → Korrigieren (`test_entity_links.py:105`)
- 4x Manually constructed user → Echte Auth verwenden (`test_external_agent_api.py:92-101`, `test_graph_rag.py`, `test_agent_memory.py`, `test_marketplace.py`)
- 1x PermissionLevelBadge hardcoded → i18n (`frontend/src/pages/SettingsRechte.tsx:37-75`)
- 1x categoryLabels/modelOptions hardcoded → Aus Backend (`frontend/src/pages/ProactiveAISettings.tsx:3-17`)
- 1x ENTITY_OPTIONS hardcoded → Aus Backend (`frontend/src/pages/CustomFields.tsx:40-43`)
---
## Phase 4: P3 Code Quality (49 Fixes, ~4h)
### 4.1 Backend P3s (20 Fixes)
- Dead Code entfernen (restore_registry `register_default_entities`, history_hooks `register_default_history_hooks`, registry `_mounted_routes`)
- Deprecated notification imports ersetzen (`entity_permission_service.py:30`, `schemas/common.py:22-58`)
- Duplicate imports entfernen (`ai_copilot_service.py:18-19`, `ai_proactive/services.py:26-27`)
- Private Attribut Zugriff ersetzen (`custom_fields.py:42``list_discovered()` statt `registry._plugins`)
- `__import__` inline durch proper import ersetzen (`entity_attachment.py:45-49`)
- Duplicate field definitions entfernen (`system_settings.py:44-54`)
- Unified Search hardcoded entity maps konsolidieren (`search_engine.py:19-24`, `lifecycle.py:22-27`, `jobs.py:16-21` → eine Registry)
- Plugin→Plugin direkte Contracts-Imports vereinheitlichen (`ai_assistant/plugin.py:96-98`, `system_notif/plugin.py:161`, `graph_rag/plugin.py:44,57` → Contract-Registry nutzen)
### 4.2 Frontend P3s (30 Fixes)
- `import * as LucideIcons` durch ICON_MAP ersetzen (2 Dateien)
- `any` type casts durch proper types ersetzen (~15 Dateien)
- inline styles durch Tailwind classes ersetzen (3 Dateien)
- hardcoded German strings durch i18n t() ersetzen (~10 Dateien)
- redundante regex vor DOMPurify entfernen
- leere DmsTrash Implementation vervollständigen
### 4.3 Test P3s (8 Fixes)
- Duplicate fixtures/decorators entfernen
- sys.path.insert durch conftest/pytest config ersetzen
- Unused imports entfernen
- Potential KeyError fixen
---
## Verifikation nach jeder Phase
1. `python -m pytest -v --tb=short` — alle Tests grün
2. `cd frontend && npx tsc --noEmit` — TypeScript kompiliert
3. `python -c "from app.main import app; print(len(app.routes))"` — App startet
4. `python scripts/check_cross_plugin_imports.py` — 0 Verstöße
---
## Aufwandsschätzung
| Phase | Fixes | Aufwand | Priorität |
|-------|-------|---------|-----------|
| 1 — P0 | 8 | ~2h | Sofort |
| 2 — P1 | 27 | ~6h | Hoch |
| 3 — P2 | 69 | ~12h | Mittel |
| 4 — P3 | 49 | ~4h | Niedrig |
| **Total** | **153** | **~24h** | |
## Reihenfolge
1. **Phase 1** — P0 Runtime Crashes (sofort, blockiert alles)
2. **Phase 2** — P1 Funktionale Fehler (nach P0)
3. **Phase 3** — P2 Architekturfehler (nach P1, kann parallel)
4. **Phase 4** — P3 Code Quality (nach P3, kann parallel)
Nach jeder Phase: Tests laufen, App startet, Cross-Plugin-Checker 0 Verstöße.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff