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:
@@ -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.**
|
||||
@@ -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.
|
||||
@@ -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
Reference in New Issue
Block a user