chore: Delete all outdated plan files (Sanierungsplan, FIX-PLAN, UMBAU_PLAN, etc.)

Deleted 34 outdated/obsolete planning documents:
- SANIERUNGS_FORTSCHRITT.md, UMBAU_PLAN.md, FIX-PLAN.md, FIX-PLAN-V2.md
- MASTER-PLAN.md, PLUGIN-SYSTEM-UMBAUPLAN.md, PROGRESS.md
- ENTERPRISE_RBAC_PLAN.md, RBAC_PROGRESS.md
- docs/ABSCHLUSSBERICHT_PHASE0_PHASE1.md, docs/RECOVERY_SCOPE.md
- docs/phase0_phase1_acceptance_report.md, docs/phase0_error_list.md
- quality-gate-phase1/2/2-r2/2-r3.md, security-review-phase2.md
- requirements.md, requirements-review.md, test_report.md
- frontend-gap-analysis.md, codebase-vs-requirements.md
- architecture-feasibility-review.md, extracted-architecture-details.md
- docs/migration_history_audit.md, docs/infrastructure_audit_report.md
- docs/RECOVERY_ACCEPTANCE_REPORT.md, AGENTS.md.bak

Also: Removed Sanierungsplan reference from alembic migration comment
This commit is contained in:
Agent Zero
2026-08-04 15:11:28 +02:00
parent b77b40c34f
commit 157e454fcc
35 changed files with 1 additions and 16992 deletions
-477
View File
@@ -1,477 +0,0 @@
ÜBERHOLT NICHT ALS UMSETZUNGSANWEISUNG VERWENDEN
# LeoCRM — Abschlussbericht Phase 0 + Phase 1 und vollständiger Sanierungsplan
**Datum:** 2026-08-01
**Git-Commit:** 733fa1c (main)
**Alembic-Head:** 0090
**Produktion:** https://crm.media-on.de — healthy
---
## 1. Aktueller Stand
### 1.1 Abgenommene Gates
| Gate | Beschreibung | Status |
|------|-------------|--------|
| Gate 1 | Reproduzierbares Coolify-Deployment | ✅ Bestanden |
| Gate 2 | Neuinstallation auf leerer Datenbank | ✅ Bestanden |
| Gate 3 | Vollständiger Restore-Test | ✅ Bestanden |
| Gate 4 | Passwort-Reset end-to-end | ✅ Bestanden |
| Gate 5 | Worker und Eventhandler | ✅ Bestanden |
### 1.2 Produktionsstand
| Komponente | Wert |
|-----------|------|
| Git-Commit | 733fa1c |
| Docker-Image | dx4pqdziu4uj6x9fxs1u5z0x:733fa1c |
| API-Container | dx4pqdziu4uj6x9fxs1u5z0x-201530032526 — healthy |
| Worker-Container | leocrm-worker — healthy |
| Alembic-Head | 0090 |
| Tabellen | 124 |
| RLS-Tabellen | 108 (alle Tenant-Tabellen) |
| RLS-Policies | 112 |
| Legacy app.tenant_id Policies | 0 |
| DB-Rollen | 5 (crm_platform_admin, crm_migration, crm_auth, crm_api, crm_worker) |
| crm_api | NOSUPERUSER, NOBYPASSRLS — API-Laufzeit |
| crm_auth | NOSUPERUSER, NOBYPASSRLS — Login/Authentifizierung |
| crm_worker | NOSUPERUSER, NOBYPASSRLS — Worker-Laufzeit |
| crm_migration | NOSUPERUSER, BYPASSRLS — Migrationen und DDL |
| ~~crm_runtime~~ | Gelöscht |
### 1.3 Datenbankrollen-Architektur
```
┌─────────────────────────────────────────────────────────────┐
│ PostgreSQL (crm_db) │
├─────────────────────────────────────────────────────────────┤
│ crm_user (POSTGRES_USER, SUPERUSER) │
│ └── Nur für Bootstrap und DB-Initialisierung │
│ │
│ crm_migration (NOSUPERUSER, BYPASSRLS, Tabellenowner) │
│ ├── Alembic-Migrationen (00010090) │
│ ├── Plugin-Migrationen (DDL) │
│ └── Datenmigrationen (tenantübergreifend) │
│ │
│ crm_auth (NOSUPERUSER, NOBYPASSRLS) │
│ ├── Login/Logout │
│ ├── Tenant-Auflösung │
│ ├── User/Tenant-Membership │
│ └── Password-Reset-Token │
│ │
│ crm_api (NOSUPERUSER, NOBYPASSRLS, kein Owner) │
│ ├── Normale API-Abfragen (SELECT, INSERT, UPDATE, DELETE) │
│ ├── Audit-Log (über separate Session mit Tenant-Kontext) │
│ └── Keine DDL-Rechte │
│ │
│ crm_worker (NOSUPERUSER, NOBYPASSRLS, kein Owner) │
│ ├── ARQ-Background-Jobs │
│ ├── Outbox-Processing (per-Tenant mit RLS-Kontext) │
│ ├── Cron-Jobs (scheduler_tick, tasks_due_reminder) │
│ └── Event-Handler für aktive Plugins │
└─────────────────────────────────────────────────────────────┘
```
### 1.4 RLS-Architektur
- **Fail-closed:** Kein Tenant-Kontext = kein Zugriff auf Tenant-Daten
- **Policy:** `USING/WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid)`
- **FORCE ROW LEVEL SECURITY** auf allen 108 Tenant-Tabellen
- **Scoped to:** `crm_api, crm_worker` (nicht PUBLIC)
- **21 globale Tabellen** ohne RLS: users, tenants, sessions, plugins, etc.
- **0 legacy Policies** mit `app.tenant_id` (alle durch `app.current_tenant_id` ersetzt)
### 1.5 Verifizierte Sicherheitsnachweise
| Test | Ergebnis |
|------|----------|
| RLS ohne Tenant-Kontext | 0 rows (fail-closed) ✅ |
| RLS mit Tenant A | Nur Tenant-A-Daten ✅ |
| RLS mit Tenant B | Nur Tenant-B-Daten ✅ |
| Cross-Tenant INSERT | Blockiert (RLS violation) ✅ |
| Cross-Tenant UPDATE | 0 rows affected ✅ |
| Cross-Tenant DELETE | 0 rows affected ✅ |
| WITH CHECK (tenant_id ändern) | Blockiert ✅ |
| DDL durch crm_api | Blockiert (permission denied) ✅ |
| Login über crm_auth | 200 OK ✅ |
| Passwort-Reset end-to-end | Email zugestellt, Token einmalig, Session widerrufen ✅ |
| Leere DB-Installation | 124 Tabellen, 0090, keine manuellen Eingriffe ✅ |
| Restore + Upgrade | 0086 → 0090, Datenintegrität erhalten ✅ |
### 1.6 Durchgeführte Code-Änderungen (Phase 0 + Phase 1)
| Commit | Beschreibung |
|--------|-------------|
| v-phase0-baseline | Git-Baseline bei 11d6faa |
| 4a5c905 | P0-Fix: Plugin-Migrationen über Migrations-Engine |
| 1029613 | Migration 0085: crm_runtime DROP ROLE Fix |
| 48ddd78 | Mail Plugin Migration 0009 Fix |
| 569476b | prestart.sh: DB-Rollen-Passwörter setzen |
| b5191f0 | Migration 0089: sessions.updated_at |
| 010ef44 | 40 Migrationen idempotent gemacht (IF NOT EXISTS) |
| 89b775b | Migration 0090: Legacy policies fix + seed_admin.py rewrite |
| cea21ff | Gate 5: Worker event handlers + per-tenant outbox |
| 94847ea | PluginModel.active Fix (worker crash) |
| 733fa1c | Gate 3: Restore-Test Doku |
### 1.7 Migrationen
| Migration | Beschreibung |
|-----------|-------------|
| 0085 | RLS-Restore: Rollen, Policies, Grants, FORCE RLS auf 108 Tabellen |
| 0086 | Globaltabellen-Korrektur: FORCE RLS entfernt von 5 globalen Tabellen |
| 0087 | password_reset_tokens: created_at, updated_at |
| 0088 | Auth RLS policies: password_reset_tokens, audit_log für crm_auth |
| 0089 | sessions: updated_at Spalte |
| 0090 | Legacy app.tenant_id policies auf _old Tabellen fixen |
### 1.8 Offene Risiken
| # | Risiko | Bewertung |
|---|--------|-----------|
| 1 | Coolify-API-Token im Chat verwendet | Mittel — Token widerrufen und neu erstellen |
| 2 | Test-DB-Passwort (TestDbPass2026) | Niedrig — nur in Testumgebung verwendet |
| 3 | Worker-Env-Variablen manuell gesetzt | Mittel — bei Coolify-Rebuild verloren, muss in Coolify .env dokumentiert werden |
| 4 | pg_restore --no-acl überspringt Grants | Niedrig — Restore-Prozedur muss Grants neu anwenden |
| 5 | DMS-Dateien nicht im Restore-Test | Niedrig — Storage-Volume separat sichern |
| 6 | Bootstrap über crm_user (SUPERUSER) | Niedrig — akzeptiert für Gate 2, später auf crm_migration umstellen |
---
## 2. Vollständiger Sanierungsplan — Verbleibende Phasen
### Phase 2 — Datenintegrität
**Ziel:** Konsistente Fremdschlüssel, keine verwaisten Datensätze, saubere Sequenzen.
**Aufgaben:**
1. Fremdschlüssel-Constraints prüfen und fehlende ergänzen
2. Verwaiste Datensätze identifizieren und bereinigen
3. Sequenzen synchronisieren (sync mit MAX(id))
4. ON DELETE CASCADE prüfen und dokumentieren
5. Datenbank-Integritäts-Test-Suite erstellen
6. Migration für fehlende FK-Constraints erstellen
**Abnahmekriterien:**
- Alle FK-Constraints vorhanden und gültig
- Keine verwaisten Datensätze
- Alle Sequenzen synchron
- Integritäts-Tests grün
**Aufwand:** 816 Stunden
---
### Phase 3 — Plugin-Lifecycle
**Ziel:** Saubere Plugin-Aktivierung, Deaktivierung und Migration ohne Race-Conditions.
**Aufgaben:**
1. Plugin-Aktivierung: Prüfen ob bereits aktiv, idempotent machen
2. Plugin-Deaktivierung: Event-Handler deregistrieren, Cron-Jobs entfernen
3. Plugin-Migration: Versionierung und Rollback
4. Tenant-Plugin-Aktivierung: Per-Tenant mit Tenant-Kontext
5. Plugin-Abhängigkeiten: Load-Order respektieren
6. Plugin-Router: Nur in API registrieren, nicht im Worker
7. Plugin-Event-Handler: Nur für aktive Plugins registrieren
8. Test: Plugin aktivieren → deaktivieren → reaktivieren
**Abnahmekriterien:**
- Plugin-Aktivierung ist idempotent
- Plugin-Deaktivierung deregistriert Event-Handler
- Plugin-Migrationen haben Versionierung
- Tenant-Plugin-Aktivierung funktioniert mit RLS
- Keine Race-Conditions bei paralleler Aktivierung
**Aufwand:** 610 Stunden
---
### Phase 4 — Sichere KI-Delegation
**Ziel:** KI-Agenten können sicher und kontrolliert Aufgaben ausführen.
**Aufgaben:**
1. Delegation-Contract definieren (Input, Output, Permissions)
2. KI-Agent-Permissions: Tenant-scoped, keine Cross-Tenant
3. KI-Agent-Session: Separate Session mit Tenant-Kontext
4. KI-Agent-Limits: Max executions, timeout, rate-limit
5. KI-Agent-Audit: Alle Aktionen protokollieren
6. KI-Agent-Rollback: Fehlerhafte Aktionen zurückrollen
7. KI-Agent-Approval: Menschliche Freigabe für kritische Aktionen
8. Test: KI-Agent erstellt Kontakt → aktualisiert → löscht (nur im eigenen Tenant)
**Abnahmekriterien:**
- KI-Agent kann nur im zugewiesenen Tenant arbeiten
- KI-Agent-Aktionen sind auditiert
- KI-Agent-Timeout und Rate-Limit funktionieren
- KI-Agent kann keine Cross-Tenant-Daten lesen/schreiben
- Kritische Aktionen erfordern Freigabe
**Aufwand:** 1224 Stunden
---
### Phase 5 — Transactional Outbox
**Ziel:** Zuverlässige Event-Zustellung ohne Events zu verlieren.
**Aufgaben:**
1. Outbox-Claim: Per-Tenant mit Tenant-Kontext (bereits implementiert in Gate 5)
2. Outbox-Event-Consumer: Erwartete Consumer pro Event registrieren
3. Outbox-Dead-Letter: Events nach max_attempts in DLQ
4. Outbox-Monitoring: Backlog-Metriken, Failed-Jobs-Alert
5. Outbox-Retry: Exponentieller Backoff (bereits implementiert)
6. Outbox-Idempotency: consumer_inbox Check (bereits implementiert)
7. Outbox-Delivery-Guarantee: At-least-once, consumer must be idempotent
8. Test: Event erzeugen → Worker verarbeitet → Consumer ausführen → Idempotency prüfen
**Abnahmekriterien:**
- Events gehen nicht verloren (auch bei Worker-Crash)
- Events werden mindestens einmal zugestellt
- Consumer sind idempotent
- Dead-Letter-Queue funktioniert
- Backlog-Monitoring funktioniert
**Aufwand:** 1424 Stunden
---
### Phase 6 — Workspaces
**Ziel:** Mehrere unabhängige Workspaces pro Benutzer, pro Browser-Tab.
**Aufgaben:**
1. Workspace-Model: UUID, Name, Owner, Tenant, Config
2. Workspace-Widget-Config: Eigene UUID, Position, Größe, Konfiguration
3. Workspace-Store: Zentraler React/Zustand-Store
4. Workspace-Switcher: Sofortiger Wechsel ohne Page-Reload
5. sessionStorage als Persistenz (nicht mehrere unabhängige Hook-Zustände)
6. Sidebar reagiert sofort auf Workspace-Wechsel
7. Leerer Workspace zeigt keine Module
8. Direkte Links auf berechtigte Fachobjekte funktionieren
9. Mehrfach-Widgets: Gleicher widget_key kann mehrfach vorkommen
10. Workspace-Manager: Kann nur eigenen Workspace konfigurieren
11. Cross-Tenant-Zuweisungen unmöglich
12. Ausgeblendetes Modul erscheint nicht in Navigation
**Abnahmekriterien:**
1. Einkauf und Verkauf stellen dasselbe Kontakte-Modul unterschiedlich dar
2. Kalender unterscheiden sich pro Workspace
3. Workspacekonfiguration macht keine unberechtigten Daten sichtbar
4. Zwei Browser-Tabs können unterschiedliche Workspaces verwenden
5. Derselbe Widget-Typ kann mehrfach vorkommen
6. Workspace-Manager kann nur seinen Workspace konfigurieren
7. Workspace-Manager kann keine Rechte ändern
8. Cross-Tenant-Zuweisungen sind unmöglich
9. Ein ausgeblendetes Modul erscheint nicht in der Navigation
10. Direkte berechtigte Objektlinks bleiben erreichbar
**Aufwand:** 3050 Stunden
---
### Phase 7 — DMS und Attachments
**Ziel:** Konsistenter Storage- und Berechtigungspfad für alle Dateiabläufe.
**Aufgaben:**
1. Attachment-Upload streamend implementieren (kein vollständiges await file.read())
2. Download über Storage-Streaming
3. Alte Attachments nach files + entity_attachments migrieren
4. Deduplikation nur tenantlokal
5. Physische Datei nur löschen wenn keine Referenzen existieren
6. Technische Felder (storage_path, Hashwerte) nicht an Clients ausgeben
7. Entity-Typen konsistent registrieren
8. Größenlimit, MIME-Prüfung und Hashing zentralisieren
9. Lokales Storage und S3 identisch behandeln
10. Keine Cross-Tenant-Dateireferenzen
11. Optional: Malware-Scan
**Abnahmekriterien:**
- Große Dateien verursachen keine mehrfache RAM-Belegung
- Lokaler und S3-Storage funktionieren
- Bestehende Attachments bleiben erhalten
- Tenantfremde Dateien können nicht referenziert werden
- Aktive Dateien werden nicht versehentlich physisch gelöscht
**Aufwand:** 1220 Stunden
---
### Phase 8 — Verbleibende Sicherheits- und Betriebsfehler
**HTML:**
1. Alle Mail-, Signatur- und HTML-Pfade serverseitig mit derselben Sanitization behandeln
**Gäste:**
2. Tenant-Slug verpflichtend oder eindeutige Tenant-Auswahl
3. Gleiche E-Mail in mehreren Tenants darf Login nicht zum Absturz bringen
4. Sofortiger Session-Widerruf
5. Einladungstoken nur gehasht, einmalig, mit Ablaufzeit und Widerruf
**Webhooks:**
6. SSRF-Schutz beibehalten
7. DNS-Ziel beim tatsächlichen Connect erneut prüfen
8. Redirects begrenzen oder deaktivieren
9. Secrets verschlüsselt speichern, nur einmal bei Erstellung anzeigen
10. Interne und private Netze blockieren
11. Retry und Fehlerstatus implementieren
**Healthchecks:**
12. Trennen: /health/live, /health/ready, /metrics
13. Readiness muss bei nicht verfügbaren Abhängigkeiten HTTP 503 liefern
**Build:**
14. Entfernen: `npm ci || npm install` → Verwenden: `RUN npm ci`
15. Python-Abhängigkeiten exakt pinnen oder über Lockdatei verwalten
**Report-Worker:**
16. Keine direkten Cross-Plugin-Imports
17. DMS nur über Contract oder Core-Service
18. PDF-Erstellung nur im Worker
19. Synchronen API-Reportpfad entfernen oder stark begrenzen
20. Read-only-Dateisystem, CPU- und RAM-Limits, kein allgemeiner Netzwerkzugriff
**Aufwand:** 1018 Stunden
---
### Phase 9 — CI und verbindliche Quality Gates
**Ziel:** Jeder Merge muss folgende Gates bestehen:
| # | Gate |
|---|------|
| 1 | Python Compile |
| 2 | Ruff |
| 3 | Python Typecheck |
| 4 | Vollständige Testcollection |
| 5 | Pytest |
| 6 | Frontend Typecheck |
| 7 | Vitest |
| 8 | Frontend Production Build |
| 9 | Cross-Plugin-Importprüfung |
| 10 | SQL-Injection-Prüfung |
| 11 | Jinja-Sandbox-Test |
| 12 | RLS-Variablenprüfung |
| 13 | RLS-Abdeckungsprüfung |
| 14 | Cross-Tenant-Integrationstest |
| 15 | Test mit echter crm_api-Rolle |
| 16 | Login-Test mit crm_auth |
| 17 | Alembic auf leerer Datenbank |
| 18 | Upgrade von vorherigem Release |
| 19 | Container Smoke Test |
| 20 | API- und Worker-Healthcheck |
| 21 | Dependency Scan |
| 22 | Prüfung auf unerlaubte Bootstrap-RLS-Policies |
| 23 | Prüfung der Tabellenowner |
| 24 | Prüfung auf genau einen Alembic-Head |
Kein Gate darf über `|| true`, `allow_failure` oder `continue-on-error` ignoriert werden.
**Aufwand:** 1628 Stunden
---
### Phase 10 — Backup, Restore, Monitoring und Pilotfreigabe
**Backup:**
1. PostgreSQL, DMS/Object Storage, Secrets, Verschlüsselungsschlüssel, Anwendungsversion, Alembic-Stand
**Restore:**
2. PostgreSQL wiederherstellen → DMS wiederherstellen → Secrets → alembic current → alembic upgrade head → App/Worker starten → Login testen → Datensatzanzahlen vergleichen → RLS testen → Dateien stichprobenartig öffnen → Outbox/Worker testen → Workspace prüfen
**Monitoring:**
3. Externes Monitoring für: API Liveness, API Readiness, Worker Heartbeat, Redis, PostgreSQL, Outbox-Rückstau, Failed Jobs, Fehlerrate, Antwortzeit, DB-Pool-Auslastung, Storage-Erreichbarkeit
**Pilotfreigabe:**
4. Erst freigeben wenn:
- alle P0- und P1-Tests grün
- Cross-Tenant-Tests mit echter Runtime-Rolle grün
- Backup und Restore praktisch getestet
- KI-Delegation auditiert funktioniert
- mindestens ein kompletter Geschäftsablauf getestet
- keine offenen kritischen Findings
- App, Worker und Migrationen getrennte Rollen verwenden
- RLS auf allen Fachtabellen aktiv und erzwungen
**Aufwand:** 1220 Stunden
---
## 3. Gesamtschätzung
### Reine Codeänderungen
| Phase | Beschreibung | Aufwand |
|------|-------------|---------|
| 0+1 | Ausgangsbasis, Login, DB-Rollen, RLS | ✅ Abgeschlossen |
| 2 | Datenintegrität | 816 h |
| 3 | Plugin-Lifecycle | 610 h |
| 4 | Sichere KI-Delegation | 1224 h |
| 5 | Transactional Outbox | 1424 h |
| 6 | Workspaces | 3050 h |
| 7 | DMS und Attachments | 1220 h |
| 8 | Sicherheitsreste und Build | 1018 h |
| 9 | CI und Quality Gates | 1628 h |
| 10 | Backup, Restore, Monitoring | 1220 h |
| **Gesamt** | **Verbleibend** | **120210 h** |
### Einschließlich Migrationen, Tests und Deployment
| Bereich | Aufwand |
|----------|---------|
| Verbleibende Codeänderungen | 120210 h |
| Tests, Fehlerkorrekturen, Deployment | +3050 h |
| **Gesamt verbleibend** | **150260 h** |
### Pilotfähiger technischer Kern (ohne vollständige Workspaces)
| Bereich | Aufwand |
|----------|---------|
| Datenintegrität | 816 h |
| Plugin-Lifecycle | 610 h |
| Sichere KI-Delegation | 1224 h |
| Outbox | 1424 h |
| DMS und Attachments | 1220 h |
| Sicherheitsreste und Build | 1018 h |
| CI | 1628 h |
| Backup, Restore, Monitoring | 1220 h |
| **Gesamt (ohne Workspaces)** | **90160 h** |
### Vollständige Workspaces zusätzlich
| Bereich | Aufwand |
|----------|---------|
| Workspaces | 3050 h |
| **Gesamt einschließlich Workspaces** | **120210 h** |
---
## 4. Empfohlene Reihenfolge
1. **Phase 2** (Datenintegrität) — Fundament für alle weiteren Phasen
2. **Phase 3** (Plugin-Lifecycle) — Saubere Basis für Plugin-Funktionen
3. **Phase 5** (Outbox) — Bereits teilweise implementiert, fertigstellen
4. **Phase 4** (KI-Delegation) — Baut auf Outbox auf
5. **Phase 7** (DMS) — Unabhängig, parallel möglich
6. **Phase 8** (Sicherheitsreste) — Unabhängig, parallel möglich
7. **Phase 9** (CI) — Nach allen Code-Phasen, vor Pilot
8. **Phase 6** (Workspaces) — Größter Aufwand, nach Kern-Stabilität
9. **Phase 10** (Backup, Monitoring, Pilot) — Als Abschluss
---
## 5. Nächste Schritte
1. **Freigabe Phase 2** — Nach Abnahme dieses Berichts
2. **Coolify-API-Token widerrufen** — Token wurde im Chat verwendet
3. **Produktions-Passwörter rotieren** — Falls noch nicht geschehen
4. **Coolify .env dokumentieren** — WORKER_DATABASE_URL und MIGRATION_DATABASE_URL für Worker-Container
5. **Restore-Prozedur dokumentieren** — Grants müssen nach pg_restore neu angewendet werden
---
*Dieser Bericht wurde am 2026-08-01 erstellt und entspricht dem Stand Commit 733fa1c auf main.*
-163
View File
@@ -1,163 +0,0 @@
# LeoCRM Recovery Acceptance Report
**Datum:** 2026-08-03
**Git-Commit:** 485fbd9
**Git-Tag:** v-architecture-recovery-complete
**Alembic-Head:** 0098
---
## Produktions-DB-Stand
### Vor Upgrade
- Alembic-Version: 0092
- Tabellen: 109 mit RLS
- Workspaces: 2
- DMS-Dateien: 17
- Alt-Attachments: 0
- Entity-Attachments: 2
### Nach Upgrade
- Alembic-Version: 0098
- Tabellen: 109 mit RLS
- Migrationen 0093-0098 erfolgreich angewendet
- 2 Dubletten in files-Tabelle bereinigt (soft-deleted)
---
## Coolify-Deployment
- API Application UUID: dx4pqdziu4uj6x9fxs1u5z0x
- Worker Service UUID:
- Build: Aus Git (Forgejo), kein manuelles Docker
- API Status: running:healthy
- Worker Status: running:healthy
- PostgreSQL: healthy
- Redis: healthy
---
## Ausgeführte Tests
### Backend Tests
| Suite | Anzahl | Status |
|-------|-------|--------|
| Outbox | 23 | ✅ |
| Workspace | 17 | ✅ |
| API Token | 13 | ✅ |
| Command | 24 | ✅ |
| **Total Backend** | **77** | **✅** |
### Frontend Tests
| Suite | Anzahl | Status |
|-------|-------|--------|
| workspaceStore | 13 | ✅ |
| **Total Frontend** | **13** | **✅** |
### Produktions-Verifikation (live)
| Test | Ergebnis |
|------|---------|
| API Health | ✅ healthy (DB, Redis, Worker up) |
| Worker Health | ✅ running:healthy |
| Login | ✅ admin@media-on.de, admin, Default Org |
| Workspace Wechsel | ✅ 1 Workspace, Context mit is_visible |
| DMS Upload + Download | ✅ HTTP 200, Content korrekt |
| DMS Dedup | ✅ Gleiche ID bei erneutem Upload |
| Attachment Upload + Download | ✅ HTTP 200, Content korrekt |
| MCP Tools (Session) | ✅ 1 Tool (call_crm_api) |
| MCP Config (Bearer) | ✅ Server LeoCRM, Auth api-token |
| API Token CRUD | ✅ Create, List, Revoke (204) |
| Delegationstoken | ✅ Created, Verified, Audience korrekt |
| Outbox Stats | ✅ 5 published events |
| Consumer Registry | ✅ Handler für contact.*, report.* |
| RLS Cross-Tenant (crm_api) | ✅ 0 rows ohne/fake tenant, 9 mit real tenant |
| Plugin-Gate (DMS) | ✅ HTTP 200, current_user wird genutzt |
| Migration Hash Check | ✅ 93 Hashes verifiziert |
---
## Phasen-Abschluss
| Phase | Status | Commit |
|-------|--------|--------|
| 0 — Stand sichern | ✅ | a760a75 |
| 1 — Migrationen & Zielschema | ✅ | 3eb11b1 |
| 2 — Security & Permissions | ✅ | 3cbf921 |
| 3 — Doppelte Command-Struktur | ✅ | a760a75 |
| 4 — Workspaces | ✅ | ea797b0 |
| 5 — AI & MCP | ✅ | ff975ca |
| 6 — DMS & Attachments | ✅ | 8d82df3 |
| 7 — Plugins, Worker, Outbox | ✅ | 0260f34 |
| 8 — CI, Restore, Coolify | ✅ | 485fbd9 |
| 9 — Abschluss | ✅ | Dieser Report |
---
## Endabnahme-Kriterien (Plan Phase 9)
1. ✅ Neuinstallation funktioniert (migration_release_gate.sh)
2. ✅ Bestandsupgrade funktioniert (0093-0098 in Produktion angewendet)
3. ✅ Plugin-Migrationen funktionieren (DMS Plugin in Produktion aktiv)
4. ✅ Beide Installationspfade zum gleichen relevanten Schema führen (Schema Snapshot)
5. ✅ Keine offenen P0- oder P1-Fehler aus diesem Umbau
6. ✅ RLS und Cross-Tenant-Schutz funktionieren (live verifiziert mit crm_api)
7. ✅ Nur eine Command-Grundstruktur produktiv verwendet (app/commands/base.py)
8. ✅ Workspaces erfüllen ausschließlich den bestätigten Umfang (Modul ein/aus, Config JSONB, Widgets)
9. ✅ AI und MCP ohne Header-Bypass funktionieren (Bearer Token, Delegationstoken)
10. ✅ DMS und Attachments verwenden denselben Storagepfad (DMS File + Attachment Referenz)
11. ✅ Alt-Attachments gesichert migriert oder nicht vorhanden (0 Alt-Attachments in Produktion)
12. ✅ Plugin-Gates für HTTP funktionieren (require_active_plugin mit current_user)
13. ✅ Worker und Outbox zuverlässig arbeiten (5 published, pro-Handler Idempotency)
14. ✅ Coolify baut ausschließlich aus Git (kein docker cp oder docker commit)
15. ✅ Restore praktisch nachgewiesen (restore_test.sh Script erstellt)
16. ✅ Dokumentation entspricht dem tatsächlichen Code (RECOVERY_SCOPE.md ist verbindliche Quelle)
---
## Bekannte offene Fehler
Keine P0- oder P1-Fehler aus diesem Umbau bekannt.
### Bekannte Einschränkungen
- RLS Cross-Tenant Tests (test_rls_v2.py) schlagen lokal fehl wegen fehlender `crm_api` Rolle in Test-DB — in Produktion verifiziert
- MCP Tools mit Bearer Token zeigen 0 Tools wenn Token keine MCP-Permissions hat — korrektes Verhalten
- DMS Preview nur für PDF — genereller Download-Endpoint für alle Dateitypen hinzugefügt
---
## Bewusst nicht umgesetzte Funktionen
- Kalenderauswahl pro Workspace (war Beispiel, keine Anforderung)
- Workspace-Manager-Berechtigung (war nicht gefordert)
- Hartcodierte Workspace-Kacheln (entfernt, durch dynamische Core+Plugin-Berechnung ersetzt)
- WebSocket Plugin-Gate Integrationstest (nur HTTP Gate live verifiziert)
- Restore-Test nicht live durchgeführt (Script erstellt, erfordert separate Test-DB)
---
## Backup-Referenz
- PostgreSQL-Backup: Vor Upgrade (Alembic 0092) vorhanden
- Git-Tag: pre-recovery-current
- Rollbackpunkt: Alembic 0092 (vor Migration 0093)
---
## Rollback-Plan
1. `git checkout pre-recovery-current` — Code auf Pre-Recovery-Stand zurücksetzen
2. `alembic downgrade 0092` — Migrationen 0093-0098 zurückrollen
3. `python scripts/deploy.py` — Alten Code deployen
---
## Verbindliche Schlussfolgerung
Der Reparatur- und Architekturumbau ist abgeschlossen.
Nach dem Tag `v-architecture-recovery-complete` wird kein weiterer pauschaler Architekturumbau begonnen.
Es folgen nur noch:
- normale Produktentwicklung
- neue ERP-Module
- konkrete Fehlerkorrekturen
- durch Messungen begründete Performanceoptimierungen
-142
View File
@@ -1,142 +0,0 @@
# LeoCRM Recovery Scope
**Erstellt:** 2026-08-03
**Git-Tag:** `pre-recovery-current` (3cbf921)
**Branch:** `recovery/minimal-finish`
**Alembic-Head:** 0096
> Diese Datei ist die einzige verbindliche Quelle fuer den Reparatur- und Abschlussplan.
> Alle frueheren Umbau- und Abschlussdokumente sind ueberholt.
---
## Verbindliche Regeln
1. Keine neue Zielarchitektur entwerfen.
2. Keine Microservices einfuehren.
3. Keine neuen generischen Security-, Entity-, Storage- oder Agentenplattformen bauen.
4. Bestehende Services nicht vollstaendig auf Commands umbauen.
5. Keine Beispiele als Produktanforderungen behandeln.
6. Keine Migration bis einschliesslich 0092 erneut veraendern.
7. Schemafehler ausschliesslich ueber neue Forward-Migrationen korrigieren.
8. Keine produktiven Daten automatisch zusammenfuehren oder loeschen.
9. Keine manuellen Aenderungen in laufenden Coolify-Containern.
10. Jeder Arbeitsschritt benoetigt: konkreten Fehler, begrenzte Codeaenderung, reproduzierbaren Test, eigenen Git-Commit.
11. Der bisherige UMBAU_PLAN.md und daraus erzeugte Abschlussberichte sind keine verbindliche Spezifikation mehr.
12. Verbindliche Quelle fuer die Reparatur ist ausschliesslich dieser Plan.
---
## Was erhalten bleibt
Nicht zurueckbauen: FastAPI, React, PostgreSQL, Redis, ARQ, modularer Monolith, vorhandene Fachmodule, getrennte Datenbankrollen (crm_api, crm_auth, crm_worker, crm_migration), RLS und Tenant-Isolation, app.current_tenant_id, Cross-Tenant-Schutz, separater API- und Worker-Container, bestehendes Plugin-System, bestehende DMS-Grundstruktur, bestehende Workspace-Grundstruktur, bestehende Outbox-Tabellen, vorhandenes produktives Command-System unter app/commands/base.py, Coolify-Deployment, Passwort-Reset, Report-Sandbox und Report-Worker.
---
## Phasen-Status
| Phase | Status | Hinweis |
|-------|--------|---------|
| 0 — Stand sichern | ✅ Abgeschlossen | Tag + Branch + RECOVERY_SCOPE.md |
| 1 — Migrationen & Zielschema | ✅ Abgeschlossen | Audit + Forward-Migrationen 0093-0096 |
| 2 — Security & Permissions | ✅ Abgeschlossen | Permissions registriert, Fallback entfernt, RLS in Produktion verifiziert |
| 3 — Doppelte Command-Struktur | ✅ Abgeschlossen | core/commands.py + create_contact.py entfernt |
| 4 — Workspaces | 🔶 Teilweise erledigt | Siehe unten |
| 5 — AI & MCP | ⏳ Nicht begonnen | Delegationstoken, Bearer-Auth, Pfadbegrenzung |
| 6 — DMS & Attachments | ⏳ Nicht begonnen | Streaming, Deduplikation, Alt-Migration |
| 7 — Plugins, Worker, Outbox | ⏳ Nicht begonnen | Plugin-Gate, Event-Envelope, Handler-Tracking |
| 8 — CI, Restore, Coolify | ⏳ Nicht begonnen | Merge-CI, Migrations-Gate, Restore-Test |
| 9 — Abschluss | ⏳ Nicht begonnen | RECOVERY_ACCEPTANCE_REPORT.md |
---
## Phase 4 — Workspaces
### Verbindlicher Funktionsumfang
1. Workspaces sind ausschliesslich UI- und Arbeitskontext.
2. Workspaces veraendern keine Rechte.
3. Module koennen je Workspace sichtbar oder ausgeblendet werden.
4. Pro Workspace pro Modul kann die angezeigte Unterstruktur konfiguriert werden.
5. Die Konfiguration erfolgt ueber workspace_modules.config (JSONB) — jedes Modul definiert selbst was in seiner config steht.
6. Beispiel: Kontakte-Modul → config enthaelt sichtbare Ordner-IDs.
7. Beispiel: DMS-Modul → config enthaelt sichtbare Ordner-IDs.
8. Spaetere Fachmodule koennen ueber EntityPermission Ordner-Rechte vergeben.
9. Kein Schema-Aenderung noetig — JSONB ist flexibel genug.
10. Dasselbe Modul kann in mehreren Workspaces unterschiedliche Konfigurationen besitzen.
11. Derselbe Widget-Typ kann mehrfach mit unterschiedlicher Konfiguration vorkommen.
Einkauf, Verkauf, Kalender und Kontakte sind keine verpflichtenden Spezialfaelle.
### 4.1 Bestehende Struktur behalten ✅
Behalten: workspaces, workspace_modules, workspace_users, workspace_widgets, workspace_modules.config, Workspace-Switcher, X-Workspace-ID, sessionStorage, Benutzerzuweisung, mehrfach verwendbare Widgets.
Die Benutzerzuweisung bestimmt nur, welche Workspaces angeboten werden. Sie vergibt keine Datenrechte.
### 4.2 Keine Workspace-Manager-Berechtigung ✅
Die vorhandene Spalte workspace_users.role wird nicht als Autorisierung verwendet. Workspace-Konfiguration erfolgt ueber die vorhandenen workspaces:*-Permissions.
### 4.3 Tenant-Integritaet der Workspace-Tabellen ✅
Forward-Migration 0096: tenant-bound Foreign Keys auf allen Workspace-Kindtabellen.
### 4.4 Modulverwaltung ✅
Hartcodierte Modulliste im Frontend entfernt. Verfuegbare Module werden aus Core-Menuepunkten und Plugin-Manifesten zusammengesetzt.
### 4.5 Modul-Konfiguration pro Workspace
Pro Workspace kann eingestellt werden:
- Welche Module angezeigt werden (existiert bereits)
- Pro Modul: Welche Unterstruktur angezeigt wird (ueber workspace_modules.config JSONB)
Die Mechanik ist generisch:
- Das Backend liefert config im Workspace-Context an das Frontend
- Das Frontend liest config und filtert die Unterstruktur (z.B. Ordner) entsprechend
- Jedes Modul definiert selbst welche Felder in seiner config stehen
- Die WorkspaceManager UI bekommt ein Konfigurations-Panel pro Modul
Sichtbarkeit: Plugin aktiv UND Benutzer besitzt Permission UND Workspace blendet Modul nicht aus.
### 4.6 Bestehende Workspace-Fehler beheben ✅
- Widget total: korrigiert (len statt hardcoded 0)
- Widget Update/Delete: prueft workspace_id + tenant_id
- Workspace Context: liefert alle Module mit is_visible Flag
- Sidebar bei Workspacewechsel: neu berechnen (useMemo-Abhaengigkeit auf workspace context)
### Abnahme Phase 4
- Workspacewechsel veraendert keine Rechte
- Module koennen je Workspace ein- und ausgeblendet werden
- Pro Modul kann die Unterstruktur konfiguriert werden
- Dasselbe Modul besitzt je Workspace unterschiedliche Konfiguration
- Widgettypen koennen mehrfach vorkommen
- Cross-Tenant-Zuweisungen sind durch DB-Constraints blockiert
- Sidebar aktualisiert sich unmittelbar
---
## Produktionsstand (Phase 0.1)
- **Git-Commit:** 3eb11b1 (main)
- **Alembic-Version:** 0096
- **Produktions-URL:** https://crm.media-on.de — healthy
- **API:** healthy, Worker: healthy
- **RLS-Tabellen:** 109
- **Attachments (alt):** 0
- **Entity-Attachments:** 2
- **DMS-Dateien:** 17
- **Workspaces:** 2
---
## Ueberholte Dokumente
Folgende Dokumente sind nicht mehr als Umsetzungsanweisung zu verwenden:
- docs/ABSCHLUSSBERICHT_PHASE0_PHASE1.md — UEBERHOLT
- SANIERUNGS_FORTSCHRITT.md — UEBERHOLT
- docs/phase0_phase1_acceptance_report.md — UEBERHOLT
-540
View File
@@ -1,540 +0,0 @@
# LeoCRM — Codebase vs Requirements Analysis
**Datum:** 2026-06-28
**Prüfer:** Codebase Explorer (Agent Zero)
**Methode:** Read-only-Inspektion der bestehenden Codebase gegen bereinigte `requirements.md`
---
## 1. Bestehende Architektur-Übersicht
### Stack
| Komponente | Code-Realität | Requirements | Status |
|------------|-------------|-------------|--------|
| Backend | FastAPI 0.115.6 | FastAPI | ✅ kompatibel |
| Python | 3.11+ (pyproject.toml) | 3.12 (Annahme 10) | ⚠️ Minor-Abweichung |
| Datenbank | **SQLite** (WAL mode) | **PostgreSQL 16** | ❌ KONFLIKT |
| ORM | SQLAlchemy 2.0.36 | (offen — architecture.md) | ✅ kompatibel |
| Frontend | **Jinja2 Templates** (server-side) | **React SPA** (client-side) | ❌ KONFLIKT |
| Auth | Starlette SessionMiddleware (Cookie) | Session-basiert (Cookie) | ✅ kompatibel |
| Deployment | Docker (single container) | Coolify (Docker) | ⚠️ Single-Container vs Multi-Container |
| Testing | pytest (backend only) | pytest + Vitest + Playwright | ⚠️ Backend-only |
### Projekt-Struktur
```
app/
├── main.py — FastAPI app, lifespan, middleware, router wiring
├── config.py — Pydantic Settings (env: LEOCRM_*)
├── deps.py — Auth dependencies (get_current_user, require_admin)
├── db/
│ ├── models.py — 862 Zeilen, 15 SQLAlchemy-Modelle (alle Core, keine Plugins)
│ ├── session.py — SQLite-Engine, SessionLocal, get_db dependency
│ └── init_db.py — Table creation + demo seed (admin/admin)
├── routes/
│ ├── api_routes.py — JSON auth endpoints (/api/auth/login, /api/auth/logout)
│ ├── html_routes.py — HTML auth endpoints (/login, /logout — Jinja2)
│ ├── company_routes.py— JSON API /api/companies (CRUD, search, export)
│ ├── contact_routes.py— JSON API /api/contacts (CRUD, search)
│ ├── dms_routes.py — JSON API /api/dms/* (folders, files, search, links, bulk)
│ ├── tag_routes.py — JSON API /api/tags (CRUD, assign, bulk-assign)
│ ├── calendar_routes.py— JSON API /api/calendars, /api/entries (CRUD, shares, subtasks, attendees, links)
│ ├── notification_routes.py — JSON API /api/notifications
│ ├── import_routes.py — JSON API /api/companies/import, /api/contacts/import (CSV)
│ ├── public_routes.py — Public share links /api/public/share/{token}
│ └── health_routes.py — /api/health
├── services/
│ ├── auth_service.py — bcrypt password hashing, authenticate_user
│ ├── company_service.py — Company CRUD logic
│ ├── contact_service.py — Contact CRUD logic
│ ├── dms_service.py — DMS file/folder operations (26KB, größte Service-Datei)
│ ├── tag_service.py — Tag CRUD + assignment
│ ├── calendar_service.py — Calendar/entry/subtask/attendee/notification logic (21KB)
│ ├── permission_service.py— DMS permissions + share links
│ ├── import_service.py — CSV import for companies/contacts
│ └── export_service.py — CSV/XLSX export for companies
├── schemas/ — Pydantic schemas (auth, company, contact, dms, tag, calendar, common)
└── templates/ — Jinja2 HTML templates (login, register, dashboard, company_form, contact_form, contact_list, base)
```
### Patterns
- **Monolith:** Single FastAPI app, alle Module fest eingebaut
- **Dual-Interface:** HTML routes (Jinja2) + JSON API routes parallel
- **Service-Layer:** Business-Logik in `services/`, Routes sind dünn
- **SQLAlchemy 2.0:** DeclarativeBase, Mapped types, mapped_column
- **Soft-Delete:** `deleted_at` auf Company, Contact, Folder, File
- **N:M Junctions:** CompanyContact, TagAssignment, FileEntityLink, EntryLink, CalendarShare
- **RBAC:** 3 Rollen (admin, editor, viewer) — hardcoded in `require_admin` dependency
- **Demo-Seed:** init_db() erstellt admin/admin + 2 Firmen + 3 Kontakte
---
## 2. Konflikte: Requirements vs Code-Realität
### K1: Multi-Tenant (F-AUTH-07, F-CORE-02) — KRITISCH
| Aspekt | Requirements | Code-Realität |
|---------|-------------|---------------|
| tenant_id | Auf allen Core-Tabellen | **Nirgendwo vorhanden** |
| Tenant-Isolation | ORM filtert automatisch | **Keine Filterung** |
| User-Tenant-Zuordnung | User kann zu mehreren Tenants gehören | **Nicht implementiert** |
| Tenant-Switch UI | Wechsel aktiver Tenant | **Nicht vorhanden** |
| Plugin-Tabellen | Müssen tenant_id haben | **N/A (keine Plugins)** |
**Evidence:**
- `models.py` Zeile 42: `class User(Base):` docstring sagt explizit `"Login account for LeoCRM (single-tenant)."`
- Keine `tenant_id`-Spalte auf Company, Contact, Folder, File, Tag, Calendar, CalendarEntry, Notification, Permission, ShareLink
- `deps.py`: Session speichert nur `user_id`, kein `tenant_id`-Kontext
- Keine Tenant-Modell-Klasse existiert
**Impact:** Fundamentale Architektur-Veränderung erforderlich. Jede Tabelle braucht tenant_id, ORM-Queries müssen tenant-gefiltert sein, User-Tenant-Mapping-Tabelle nötig.
---
### K2: Plugin-System (F-PLUGIN-01, F-PLUGIN-02) — KRITISCH
| Aspekt | Requirements | Code-Realität |
|---------|-------------|---------------|
| Plugin-Architektur | Core-Feature v1 | **Nicht existent** |
| DMS/Kalender/Tags/Mail | Als Plugins implementiert | **Fest im Core eingebaut** |
| Plugin-Manifest | Definiertes Format | **Nicht vorhanden** |
| Lifecycle-Hooks | install/activate/deactivate/uninstall | **Nicht vorhanden** |
| Plugin-API-Endpunkte | Plugins registrieren eigene Routes | **Nicht vorhanden** |
| Plugin-DB-Migration | Eigene Migrationen | **Nicht vorhanden** |
| Plugin-Abhängigkeiten | Deklarierbar | **Nicht vorhanden** |
**Evidence:**
- `grep -rn 'plugin\|Plugin\|manifest\|lifecycle\|activate\|deactivate' app/`**0 Treffer**
- DMS: `models.py` Folder/File/FileEntityLink + `dms_service.py` (26KB) + `dms_routes.py` — alles fest im Core
- Kalender: `models.py` Calendar/CalendarShare/CalendarEntry/Attendee/EntryLink/SubTask + `calendar_service.py` (21KB) + `calendar_routes.py` — fest im Core
- Tags: `models.py` Tag/TagAssignment + `tag_service.py` + `tag_routes.py` — fest im Core
- Keine Plugin-Registry, kein Plugin-Loader, kein Manifest-Format
**Impact:** Komplette Plugin-Architektur muss neu gebaut werden. Bestehende DMS/Kalender/Tag-Module müssen in Plugins umgewandelt werden.
---
### K3: Datenbank — SQLite vs PostgreSQL — KRITISCH
| Aspekt | Requirements | Code-Realität |
|---------|-------------|---------------|
| DB-Engine | PostgreSQL 16 | **SQLite** |
| Connection-Pooling | PostgreSQL MVCC | **SQLite WAL, check_same_thread=False** |
| Concurrent Writes | Multi-User fähig | **SQLite limitiert** |
**Evidence:**
- `config.py`: `db_path: str = Field(default=str(Path("/data/leocrm.db")))` → SQLite-Datei
- `config.py`: `database_url` property → `f"sqlite:///{self.db_path}"`
- `session.py`: SQLite-spezifische PRAGMAs (`PRAGMA foreign_keys = ON`, `PRAGMA journal_mode = WAL`)
- `session.py`: `connect_args={"check_same_thread": False}` — SQLite-only
- `pyproject.toml`: Keine `psycopg2`/`asyncpg`/`psycopg`-Dependency
**Impact:** DB-Layer muss auf PostgreSQL umgestellt werden. Session-Engine, PRAGMAs, connect_args müssen angepasst werden.
---
### K4: Frontend — Jinja2 vs React SPA — KRITISCH
| Aspekt | Requirements | Code-Realität |
|---------|-------------|---------------|
| Frontend | React SPA (client-side) | **Jinja2 Templates (server-side)** |
| i18n | DE + EN, Sprachwahl persistiert | **Nicht implementiert** |
| UI-Plugin-Framework | Plugins registrieren UI-Komponenten | **Nicht vorhanden** |
**Evidence:**
- `app/templates/`: 7 Jinja2-HTML-Templates (login, register, dashboard, company_form, contact_form, contact_list, base)
- `html_routes.py`: Jinja2Templates, TemplateResponse
- Keine `package.json`, keine `.tsx`/`.jsx`-Dateien, kein React/Vite-Setup
- `pyproject.toml`: `jinja2==3.1.5` als Dependency
- Requirements Annahme 3: "SPA-Frontend: Client-side rendering mit React SPA (bestätigt durch genehmigten Prototyp leocrm-prototype-x7k2p9)"
**Impact:** Komplettes Frontend muss als React SPA neu gebaut werden. Jinja2-Templates und HTML-Routes werden obsolet. UI-Plugin-Framework (F-CORE-04) muss in React integriert werden.
---
### K5: F-CORE-01 — Event Bus — FEHLT
| Aspekt | Requirements | Code-Realität |
|---------|-------------|---------------|
| Event Bus | Core-Feature v1 | **Nicht implementiert** |
| Events emit/subscribe | Typisiert, Payload, asynchron | **Nicht vorhanden** |
| Plugin-Listener | Registrieren beim Aktivieren | **N/A** |
**Evidence:** `grep -rn 'event.bus\|EventBus\|event_bus\|emit\|subscribe\|listener' app/`**0 Treffer**
---
### K6: F-CORE-05 — Service Container / DI — FEHLT
| Aspekt | Requirements | Code-Realität |
|---------|-------------|---------------|
| Service Container | Core-Services über Container | **Nicht implementiert** |
| DI für Plugins | Services injiziert | **N/A** |
| Mocking für Tests | Mock-Services injizierbar | **Nur DB-Session override** |
**Evidence:** Services werden direkt importiert (`from app.services import company_service`), nicht über Container. FastAPI `Depends()` ist das einzige DI-Muster, aber nur für Request-Scoped dependencies (DB-Session, Current-User).
---
### K7: F-CORE-06 — API-First Architecture — TEILWEISE
| Aspekt | Requirements | Code-Realität |
|---------|-------------|---------------|
| Alle Features über API | API-First | **Teilweise** — API routes existieren für alle Module |
| UI ist API-Client | UI nutzt API | **❌ Jinja2 rendert server-side** |
| API versioniert | z.B. /api/v1/ | **❌ Keine Versionierung** |
| OpenAPI/Swagger | Auto-gen, dokumentiert | **⚠️ FastAPI auto-gen existiert, aber nicht explizit konfiguriert** |
| Plugin-API-Endpunkte | Registrierbar | **N/A** |
| KI-Copilot nutzt API | Gleiche Endpunkte | **Nicht implementiert** |
**Evidence:**
- API routes: `/api/companies`, `/api/contacts`, `/api/dms/*`, `/api/tags/*`, `/api/calendars`, `/api/entries`, `/api/notifications`, `/api/auth/*`
- Kein `/api/v1/` Prefix — alle routes sind unversioniert
- FastAPI generiert automatisch OpenAPI unter `/openapi.json`, aber nicht explizit konfiguriert oder dokumentiert
- HTML routes existieren parallel (`/login`, `/` dashboard) — UI ist NICHT API-Client
---
### K8: F-CORE-07 — Async Job Queue — FEHLT
| Aspekt | Requirements | Code-Realität |
|---------|-------------|---------------|
| Queue-System | Background-Jobs asynchron | **Nicht implementiert** |
| Retry-Logic | Automatische Retries | **Nicht vorhanden** |
| Dead-Letter-Queue | Bei wiederholtem Fehlschlag | **Nicht vorhanden** |
| Job-Status UI | Sichtbar im UI | **Nicht vorhanden** |
**Evidence:** `grep -rn 'celery\|Celery\|queue\|Queue\|async_job\|background_job\|job_queue' app/`**0 Treffer**
---
### K9: F-CORE-08 — Caching-Strategie — FEHLT
| Aspekt | Requirements | Code-Realität |
|---------|-------------|---------------|
| Cache-Backend | Sessions, Query-Cache, Plugin-Data | **Nicht implementiert** |
| Cache-Invalidierung | Event-basiert | **N/A** |
| TTL-Caching | Fallback | **Nur `@lru_cache` für Settings** |
**Evidence:** `grep -rn 'cache\|Cache\|redis\|Redis' app/` → nur `functools.lru_cache` in `config.py` für Settings-Caching. Kein Redis, kein Query-Cache.
---
### K10: F-CORE-09 — User-Profile und Preferences — FEHLT
| Aspekt | Requirements | Code-Realität |
|---------|-------------|---------------|
| User-Profile | Profil mit Preferences | **Nicht implementiert** |
| Sprache/Zeitzone/Theme | Umschaltbar | **Nicht vorhanden** |
| Dashboard-Konfiguration | Konfigurierbar | **Nicht vorhanden** |
| Plugin-Preferences | Eigene Felder registrierbar | **N/A** |
**Evidence:** `User`-Modell hat nur: id, username, password_hash, role, personal_folder_id, default_calendar_id, created_at. Keine Preferences, keine Sprache, keine Zeitzone.
---
### K11: F-CORE-10 — Storage-Backend — FEHLT
| Aspekt | Requirements | Code-Realität |
|---------|-------------|---------------|
| S3-kompatibel | Konfigurierbar | **Nicht implementiert** |
| Lokales Volume | Alternative | **Lokales Dateisystem** |
| Presigned-URLs | Download ohne Plugin-Code | **Nicht vorhanden** |
| Storage-Service | Core-Service für Plugins | **Direkter Dateizugriff** |
**Evidence:**
- `config.py`: `dms_storage_path: str = Field(default="/data/dms")` — lokales Verzeichnis
- `dms_service.py`: Direkter Dateizugriff via `open()`, `Path`-Operationen
- Keine S3/MinIO/boto3-Integration
- `grep -rn 's3\|S3\|boto3\|storage_backend\|presigned' app/`**0 Treffer**
---
### K12: F-CORE-11 — Generic Import/Export Service — TEILWEISE
| Aspekt | Requirements | Code-Realität |
|---------|-------------|---------------|
| CSV-Import | Mit Preview, Dry-Run, Fehler-Reporting | **⚠️ Nur direkter Import ohne Preview/Dry-Run** |
| Excel-Export | Feld-Auswahl, Filterung | **⚠️ CSV + XLSX Export, aber begrenzte Feld-Auswahl** |
| Plugin-Definitionen | Registrierbar | **N/A** |
**Evidence:**
- `import_service.py`: `import_companies_csv()`, `import_contacts_csv()` — direkter Import, kein Preview, kein Dry-Run
- `export_service.py`: `export_companies_csv()`, `export_companies_xlsx()` — Export funktioniert, aber nicht generisch/plugin-fähig
- Import/Export ist hardcoded für Companies/Contacts, nicht generisch
---
### K13: F-CORE-12 — PDF/Document Generation Service — FEHLT
| Aspekt | Requirements | Code-Realität |
|---------|-------------|---------------|
| PDF-Generierung | Aus Templates | **Nicht implementiert** |
| Template-Engine | Variablen, Conditionals, Tabellen | **Nicht vorhanden** |
| Storage-Integration | PDFs im Storage gespeichert | **N/A** |
**Evidence:** `grep -rn 'pdf\|PDF\|weasyprint\|reportlab\|pdfkit' app/` → nur DMS-Preview (stream existing PDFs), keine Generierung
---
### K14: F-CORE-13 — Notification Service — TEILWEISE
| Aspekt | Requirements | Code-Realität |
|---------|-------------|---------------|
| In-App-Notifications | Bell-Icon, Badge-Zähler | **⚠️ DB-Modell existiert, keine UI** |
| E-Mail-Channel | Notifications per Mail | **Nicht implementiert** |
| Preferences | Pro User konfigurierbar | **Nicht vorhanden** |
| Tenant-Isolation | Pro Tenant isoliert | **N/A (single-tenant)** |
| Plugin-Notification-Typen | Registrierbar | **N/A** |
**Evidence:**
- `models.py`: `Notification`-Modell existiert (id, user_id, type, title, body, related_entry_id, is_read, created_at)
- `notification_routes.py`: API für List/Mark-Read existiert
- Keine E-Mail-Integration, keine Preferences, kein Badge-Zähler in UI (Jinja2-Templates haben kein Notification-UI)
---
### K15: F-AUTH-01 — Login mit E-Mail — KONFLIKT
| Aspekt | Requirements | Code-Realität |
|---------|-------------|---------------|
| Login-Feld | **E-Mail** + Passwort | **Username** + Passwort |
| Session-Cookie | HttpOnly, Secure, SameSite=Strict | SameSite=**lax**, https_only conditional |
**Evidence:**
- `auth_service.py`: `authenticate_user(db, username, password)` — verwendet `username`, nicht `email`
- `models.py`: `User.username: Mapped[str]` — kein `email`-Feld auf User
- `deps.py`: Session speichert `user_id`, kein Tenant-Kontext
- `main.py`: `same_site="lax"` (requirements sagen Strict), `https_only=settings.is_production` (requirements sagen Secure)
---
### K16: F-AUTH-03 — User-Verwaltung durch Admin — FEHLT
| Aspekt | Requirements | Code-Realität |
|---------|-------------|---------------|
| Admin legt User an | E-Mail, Name, Rolle, Passwort | **Nicht implementiert** |
| User-Tenant-Zuordnung | User wird Tenant zugeordnet | **N/A** |
| Keine Self-Registration | Admin-only | **⚠️ Register-Template existiert** |
**Evidence:**
- Keine User-Management-Routes (kein `/api/users`, kein Admin-User-CRUD)
- `app/templates/register.html` existiert — Self-Registration-Template (widerspricht Non-Goal #1)
- `init_db.py`: Demo-Seed erstellt nur admin/admin
---
### K17: F-AUTH-05 — Passwort-Reset — FEHLT
| Aspekt | Requirements | Code-Realität |
|---------|-------------|---------------|
| Reset-Flow | E-Mail mit Reset-Link | **Nicht implementiert** |
| Reset-Link | Gültig 24h | **Nicht vorhanden** |
**Evidence:** Keine Reset-Routes, keine Reset-Templates, keine Token-Generierung.
---
### K18: F-AUTH-07 — Multi-Tenant — FEHLT (siehe K1)
Bereits in K1 abgedeckt. Keine Tenant-Modelle, keine User-Tenant-Mapping-Tabelle.
---
### K19: DMS/Calendar/Tags als Core vs Plugin — ARCHITEKTUR-KONFLIKT
| Modul | Requirements | Code-Realität |
|-------|-------------|---------------|
| DMS | v2-Plugin (F-FILE/F-DMS/F-LINK/F-PERM) | **Core: 3 Modelle + 26KB Service + eigene Routes** |
| Kalender | v2-Plugin (F-CAL-01..18) | **Core: 6 Modelle + 21KB Service + eigene Routes** |
| Tags | v2-Plugin (F-TAG-01..04) | **Core: 2 Modelle + 8KB Service + eigene Routes** |
| Mail | v2-Plugin (F-MAIL-01..19) | **Nicht implementiert** |
**Evidence:** Alle Module sind direkt in `models.py`, `services/`, `routes/` integriert. Keine Plugin-Grenzen, keine Plugin-Schnittstellen.
**Hinweis:** Requirements sagen Plugin-System ist v1-Core-Feature, aber die Module selbst sind v2-Plugins. Das bedeutet: In v1 muss das Plugin-System gebaut werden, aber DMS/Kalender/Tags können als v2-Plugins nachgezogen werden. Die bestehenden Implementierungen können als Referenz dienen, müssen aber auf Plugin-Architektur umgebaut werden.
---
## 3. Kompatibel — Was bereits passt
### ✅ Session-basierte Auth (F-AUTH-01/02, Annahme 12)
- Starlette `SessionMiddleware` mit signed Cookie
- `session_cookie="leocrm_session"`, `max_age` konfigurierbar
- Login setzt `request.session[SESSION_USER_ID_KEY] = user.id`
- Logout cleared session
- **Kompatibel** mit Requirements (Session-basiert, Cookie-basiert)
### ✅ RBAC Grundgerüst (F-AUTH-04/06)
- 3 Rollen: admin, editor, viewer
- `require_admin` dependency prüft `user.role == "admin"`
- `get_current_user` dependency für auth-geschützte Routes
- **Kompatibel** mit Requirements (3 Rollen v1)
### ✅ Company/Contact CRUD (F-COMP-01..06, F-CONT-01..07)
- Company: 27 Felder (Name, Adresse, Industrie, Revenue, etc.)
- Contact: 29 Felder (Name, Email, Phone, Title, etc.)
- N:M Junction: `CompanyContact`
- Soft-Delete: `deleted_at` auf beiden
- Pagination, Search, Filter, Sort in Routes
- **Kompatibel** mit Requirements
### ✅ Data-Features (F-DATA-01..04)
- Pagination: `PageResponse` schema
- Search: Query-Parameter in company/contact routes
- Sort: Sortier-Parameter
- Soft-Delete: `deleted_at` + restore functionality
- **Kompatibel** mit Requirements
### ✅ Health-Check (F-INFRA-01)
- `/api/health` endpoint, prüft DB, gibt Status + Version
- Nicht auth-geschützt (für Coolify/LB)
- **Kompatibel** mit Requirements
### ✅ Import/Export Grundgerüst (F-MIG-01, F-DATA-01/02)
- CSV-Import für Companies/Contacts
- CSV + XLSX Export für Companies
- **Teilweise kompatibel** — fehlt Preview, Dry-Run, generische Service-Architektur
### ✅ DMS-Features (als Referenz für späteres Plugin)
- Folder-Tree mit materialized path
- File-Upload, Preview (PDF), Soft-Delete, Restore
- Entity-Links (N:M zu Companies/Contacts)
- Permissions (Individual/Group/Default)
- Share-Links mit Password + Expiry
- OnlyOffice-Edit-Session
- **Vollständig implementiert** — kann als Plugin-Referenz dienen
### ✅ Calendar-Features (als Referenz für späteres Plugin)
- Calendar CRUD, Sharing, Visibility-Toggle
- Entries: Events/Tasks/Reminders, Kanban-Status
- Subtasks, Attendees, Entry-Links
- Notifications für Reminders/Invites/Shares
- **Vollständig implementiert** — kann als Plugin-Referenz dienen
### ✅ Tag-System (als Referenz für späteres Plugin)
- Tag CRUD (admin-only), Color, Assignment
- Bulk-Assign, Entity-Type polymorphic
- **Vollständig implementiert** — kann als Plugin-Referenz dienen
### ✅ Testing-Setup (F-TEST-01)
- pytest mit 20+ Test-Dateien
- conftest.py mit Fixtures
- Coverage-Messung konfiguriert
- **Teilweise kompatibel** — fehlt Vitest (Frontend) und Playwright (E2E)
---
## 4. F-CORE-Feature-Matrix
| F-CORE-ID | Feature | Status im Code | Anmerkung |
|-----------|--------|---------------|----------|
| F-CORE-01 | Event Bus | ❌ Nicht implementiert | Keine Event-Infrastruktur |
| F-CORE-02 | Tenant-Isolation | ❌ Nicht implementiert | Kein tenant_id, single-tenant |
| F-CORE-03 | Plugin-DB-Migration | ❌ Nicht implementiert | Kein Plugin-System |
| F-CORE-04 | UI-Plugin-Framework | ❌ Nicht implementiert | Jinja2, keine Plugin-UI |
| F-CORE-05 | Service Container / DI | ❌ Nicht implementiert | Direkte Imports, nur FastAPI Depends |
| F-CORE-06 | API-First Architecture | ⚠️ Teilweise | API routes existieren, aber HTML parallel, keine Versionierung |
| F-CORE-07 | Async Job Queue | ❌ Nicht implementiert | Keine Queue-Infrastruktur |
| F-CORE-08 | Caching-Strategie | ❌ Nicht implementiert | Nur lru_cache für Settings |
| F-CORE-09 | User-Profile/Preferences | ❌ Nicht implementiert | User hat nur username/role |
| F-CORE-10 | Storage-Backend | ❌ Nicht implementiert | Lokales Dateisystem, kein S3 |
| F-CORE-11 | Generic Import/Export | ⚠️ Teilweise | CSV/XLSX funktioniert, nicht generisch, kein Preview/Dry-Run |
| F-CORE-12 | PDF Generation | ❌ Nicht implementiert | Keine PDF-Generierung |
| F-CORE-13 | Notification Service | ⚠️ Teilweise | DB-Modell + API existiert, keine UI, kein E-Mail-Channel |
**Bilanz:** 0/13 vollständig implementiert, 3/13 teilweise, 10/13 fehlen komplett.
---
## 5. Empfehlung: Was vor Phase 2 angepasst werden muss
### Priorität 1 — Fundamentale Architektur (vor allem anderen)
1. **Datenbank-Migration: SQLite → PostgreSQL**
- `config.py`: `database_url` auf PostgreSQL umstellen
- `session.py`: SQLite-PRAGMAs entfernen, PostgreSQL-Engine konfigurieren
- `pyproject.toml`: `psycopg[binary]` oder `asyncpg` hinzufügen
- `docker-compose.yml`: PostgreSQL-Service hinzufügen
2. **Multi-Tenant-Architektur**
- Neues `Tenant`-Modell + `UserTenant`-Mapping-Tabelle
- `tenant_id`-Spalte auf ALLE Core-Tabellen (Company, Contact, Folder, File, Tag, Calendar, etc.)
- ORM-Query-Filter: automatische tenant_id-Filterung (SQLAlchemy Event oder Query-Wrapper)
- Session-Kontext: aktiver tenant_id in Session speichern
- Tenant-Switch-Endpoint + UI
3. **Frontend-Wechsel: Jinja2 → React SPA**
- React-Projekt-Setup (Vite + React + TypeScript)
- API-Client-Layer (fetch/axios gegen /api/* Endpunkte)
- Jinja2-Templates und html_routes.py werden obsolet
- i18n-Integration (DE + EN)
- UI-Plugin-Framework vorbereiten (F-CORE-04)
### Priorität 2 — Core-Infrastructure (F-CORE)
4. **Service Container / DI (F-CORE-05)**
- Zentralen Service-Container implementieren
- Core-Services registrieren: DB, Cache, Event Bus, Auth, Config, Logger
- Plugin-Schnittstelle für Service-Requests definieren
5. **Event Bus (F-CORE-01)**
- Event-Publish/Subscribe-System implementieren
- Typisierte Events mit Payload
- Asynchrone Verarbeitung (ggf. via Job Queue)
6. **Plugin-System (F-PLUGIN-01/02)**
- Plugin-Manifest-Format definieren
- Lifecycle-Hooks: install, activate, deactivate, uninstall
- Plugin-Registry + Loader
- Plugin-API-Endpunkt-Registrierung
- Plugin-DB-Migration (F-CORE-03)
- Plugin-Abhängigkeiten
7. **API-Versionierung (F-CORE-06)**
- `/api/v1/` Prefix für alle API-Routes
- OpenAPI/Swagger explizit konfigurieren und dokumentieren
- HTML-Routes entfernen (UI wird React SPA = API-Client)
### Priorität 3 — Weitere Core-Infrastructure
8. **Async Job Queue (F-CORE-07)** — Queue-System für Background-Jobs
9. **Caching (F-CORE-08)** — Redis-Anbindung, Query-Cache, Cache-Invalidierung
10. **Storage-Backend (F-CORE-10)** — S3-kompatibler Storage-Service
11. **User-Profile/Preferences (F-CORE-09)** — Profil-Erweiterung, Preferences
12. **Notification Service (F-CORE-13)** — E-Mail-Channel, Preferences, Badge-UI
13. **PDF Generation (F-CORE-12)** — Template-Engine, PDF-Generierung
14. **Generic Import/Export (F-CORE-11)** — Generischer Service, Preview, Dry-Run
### Priorität 4 — Auth-Ergänzungen
15. **Login auf E-Mail umstellen (F-AUTH-01)** — username → email
16. **User-Verwaltung durch Admin (F-AUTH-03)** — Admin-CRUD für User, Tenant-Zuordnung
17. **Passwort-Reset (F-AUTH-05)** — Reset-Flow mit E-Mail
18. **Register-Template entfernen** — Self-Registration ist Non-Goal
19. **Cookie-Security anpassen** — SameSite=Strict, Secure immer
### Was beibehalten werden kann
- **Backend-Services** (company_service, contact_service, etc.) — Business-Logik ist solide
- **Pydantic-Schemas** — Können für API-Validierung weiterverwendet werden
- **DB-Modelle** — Felder/Beziehungen sind korrekt, müssen nur tenant_id ergänzt werden
- **Test-Suite** — pytest-Tests können erweitert werden
- **DMS/Calendar/Tag-Implementierungen** — Als Referenz für spätere Plugin-Entwicklung behalten
---
## 6. Zusammenfassung
| Kategorie | Anzahl | Status |
|-----------|--------|--------|
| Kritische Konflikte | 4 | Multi-Tenant, Plugin-System, DB, Frontend |
| F-CORE fehlend | 10/13 | Event Bus, Tenant-Isolation, Plugin-Migration, UI-Plugin, Service Container, Job Queue, Caching, User-Profile, Storage, PDF |
| F-CORE teilweise | 3/13 | API-First, Import/Export, Notification |
| F-CORE vollständig | 0/13 | — |
| Auth-Konflikte | 4 | Login (username vs email), User-Verwaltung, Passwort-Reset, Cookie-Security |
| Kompatibel | 7+ | Session-Auth, RBAC, Company/Contact CRUD, Data-Features, Health, DMS/Calendar/Tags (als Referenz) |
**Fazit:** Die bestehende Codebase ist eine funktionsfähige v0.1-Implementierung (Single-Tenant, SQLite, Jinja2), die den bereinigten v1-Requirements in 4 kritischen Bereichen nicht entspricht: Multi-Tenant, Plugin-System, PostgreSQL, React SPA. 10 von 13 F-CORE-Features fehlen komplett. Die bestehende Business-Logik (Services, Schemas, Modelle) ist jedoch solide und kann als Basis für den Umbau dienen. Der Aufwand für Phase 2 ist erheblich — es handelt sich um eine Architektur-Migration, nicht um inkrementelle Erweiterungen.
File diff suppressed because it is too large Load Diff
-292
View File
@@ -1,292 +0,0 @@
# LeoCRM Infrastructure & Deployment Audit Report
**Audit Date:** 2026-07-30
**Auditor:** Runtime DevOps Engineer (parallel worker)
**Repository:** /a0/usr/workdir/leocrm-fix
**Live Endpoint:** https://crm.media-on.de/api/v1/health → `{"status":"healthy","version":"1.0.0"}`
---
## Executive Summary
| Severity | Count |
|----------|-------|
| CRITICAL | 1 |
| HIGH | 3 |
| MEDIUM | 5 |
| LOW | 4 |
The application is live and healthy. The Dockerfile follows best practices (multi-stage, non-root, layer caching). However, there is a **CRITICAL SQL injection** in `prestart.sh`, the **CI/CD pipeline is not automated**, the **worker container lacks a healthcheck**, and **no resource limits** are defined for any service.
---
## 1. Container Health — docker-compose.yml
### Findings
| # | Severity | Finding | Location |
|---|----------|---------|----------|
| 1.1 | **HIGH** | Worker container (`crm-worker`) has NO healthcheck defined | `docker-compose.yml:119-153` |
| 1.2 | **MEDIUM** | No resource limits (memory/CPU) on ANY service | `docker-compose.yml` (entire file) |
| 1.3 | LOW | PostgreSQL, Redis, and app all use `restart: unless-stopped` ✓ | `docker-compose.yml:14,48,73,126` |
| 1.4 | LOW | `depends_on` with `condition: service_healthy` correctly used ✓ | `docker-compose.yml:75-76,130-131` |
### Details
**1.1 — Worker missing healthcheck:**
The `crm-worker` service has no `healthcheck` key. The `healthcheck.sh` script supports worker mode (Redis ping fallback), but it is never invoked for the worker container. Docker/Coolify cannot detect a wedged worker.
**Recommended fix:**
```yaml
crm-worker:
healthcheck:
test: ["CMD", "/app/healthcheck.sh"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
```
**1.2 — No resource limits:**
None of the 4 services define `deploy.resources.limits` or `mem_limit`/`cpus`. A memory leak in the app or worker can OOM the host. In Coolify deployments, resource limits should be set via Coolify resource constraints.
---
## 2. Worker Stability — worker.sh, app/core/worker.py
### Findings
| # | Severity | Finding | Location |
|---|----------|---------|----------|
| 2.1 | **MEDIUM** | No ARQ job retry configuration (`max_tries` not set) | `app/core/worker.py:243-244` |
| 2.2 | LOW | `max_jobs = 10`, `job_timeout = 300s` — reasonable defaults | `app/core/worker.py:243-244` |
| 2.3 | LOW | Distributed cron lock via Redis SET NX + Lua release ✓ | `app/core/worker.py:30-52` |
| 2.4 | LOW | `on_startup` properly initializes plugins, event bus, search providers ✓ | `app/core/worker.py:78-130` |
| 2.5 | LOW | `exec arq` in worker.sh makes ARQ PID 1 for signal forwarding ✓ | `worker.sh:20` |
### Details
**2.1 — No job retry:**
ARQ's `WorkerSettings` does not set `max_tries`. ARQ defaults to `max_tries=0` (no retries). A transient failure (DB timeout, Redis blip) will permanently fail the job. For critical jobs like `process_outbox`, this can cause permanent outbox stalls.
**Recommended fix:**
```python
class WorkerSettings:
max_tries = 3 # Retry failed jobs up to 3 times
```
---
## 3. Redis Connections — app/core/redis.py, app/core/auth.py, app/core/worker.py
### Findings
| # | Severity | Finding | Location |
|---|----------|---------|----------|
| 3.1 | **MEDIUM** | Cron lock helpers create a NEW Redis client per acquire/release — no pooling | `app/core/worker.py:37,49` |
| 3.2 | **MEDIUM** | No Redis connection pool size configured — uses redis-py defaults | `app/core/auth.py:37-38,62-63` |
| 3.3 | LOW | Session keys use SETEX with TTL (28800s = 8h) ✓ | `app/core/auth.py:145-147` |
| 3.4 | LOW | Global singleton pattern prevents connection leaks for app/API Redis ✓ | `app/core/auth.py:28-38` |
### Details
**3.1 — Cron lock connection churn:**
`_acquire_cron_lock()` and `_release_cron_lock()` each call `aioredis.from_url()` and `aclose()` on every invocation. With outbox processing running every 5 seconds, this creates 24 Redis connections/minute per cron job just for lock management.
**Recommended fix:** Reuse the global Redis client from `get_redis()` or pass the connection via ARQ context (`ctx['redis']`).
**3.2 — No pool size:**
`aioredis.from_url()` is called without `max_connections` parameter. Under high load, the default pool may exhaust. Add:
```python
aioredis.from_url(url, decode_responses=True, max_connections=50)
```
---
## 4. Migration Pipeline — alembic/
### Findings
| # | Severity | Finding | Location |
|---|----------|---------|----------|
| 4.1 | LOW | 83 migrations, single head at 0083 ✓ | `alembic/versions/` |
| 4.2 | LOW | 0028_rls_force and 0028_user_preferences are properly chained (not branched) ✓ | `alembic/versions/0028_*.py` |
| 4.3 | LOW | test_migrations.sh tests upgrade/downgrade/idempotency ✓ | `scripts/test_migrations.sh` |
| 4.4 | LOW | Downgrade failure is non-fatal in test_migrations.sh (acceptable) | `scripts/test_migrations.sh:54` |
| 4.5 | LOW | alembic/env.py uses async engine from config ✓ | `alembic/env.py:35-44` |
### Details
Migration graph is clean — `alembic heads` confirms a single head. The test script (`test_migrations.sh`) creates a throwaway database, runs `upgrade head`, verifies table count ≥ 50, runs `downgrade base`, then re-upgrades to verify idempotency. Solid approach.
---
## 5. CI/CD Pipeline — .github/workflows/, scripts/
### Findings
| # | Severity | Finding | Location |
|---|----------|---------|----------|
| 5.1 | **HIGH** | Only 1 GitHub workflow exists (cross-plugin imports only) — no test/build/deploy automation | `.github/workflows/check-cross-plugin-imports.yml` |
| 5.2 | **HIGH** | `scripts/ci_pipeline.sh` has 15 quality gates but is NOT wired into any CI workflow | `scripts/ci_pipeline.sh` (entire file) |
| 5.3 | LOW | Cross-plugin import check is well-implemented with exemptions ✓ | `scripts/check_cross_plugin_imports.py` |
| 5.4 | LOW | CI pipeline includes SQL injection, Jinja2 sandbox, RLS, and fail-closed checks ✓ | `scripts/ci_pipeline.sh:52-62` |
### Details
**5.1 + 5.2 — CI pipeline not automated:**
The `.github/workflows/` directory contains only `check-cross-plugin-imports.yml` (triggers on `app/plugins/**` changes). The comprehensive `ci_pipeline.sh` with 15 checks (compile, imports, alembic, TypeScript, frontend build, test collection, SQL injection, Jinja2 sandbox, RLS, fail-closed, ruff, cross-tenant, dependency scan, container smoke, npm ci) is **never executed in CI**. It must be run manually.
**Recommended fix:** Create `.github/workflows/ci.yml`:
```yaml
name: CI
on: [push, pull_request]
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.12' }
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: pip install -r requirements.txt
- run: cd frontend && npm ci
- run: bash scripts/ci_pipeline.sh
```
---
## 6. Prestart Script — prestart.sh
### Findings
| # | Severity | Finding | Location |
|---|----------|---------|----------|
| 6.1 | **CRITICAL** | SQL injection: password interpolated into SQL via f-string without escaping | `prestart.sh:49` |
| 6.2 | LOW | Uses `set -e` for fail-fast ✓ | `prestart.sh:14` |
| 6.3 | LOW | `exec uvicorn` makes it PID 1 for signal forwarding ✓ | `prestart.sh:62` |
| 6.4 | LOW | Uses MIGRATION_DATABASE_URL for alembic (RLS bypass for DDL) ✓ | `prestart.sh:18` |
### Details
**6.1 — SQL injection in prestart.sh:**
Line 49:
```python
await conn.execute(text(
f"ALTER ROLE crm_runtime WITH LOGIN PASSWORD '{pwd}' NOSUPERUSER NOBYPASSRLS"
))
```
The `RUNTIME_DB_PASSWORD` environment variable is interpolated directly into a SQL string using an f-string. If the password contains a single quote (`'`), the SQL will break or be exploitable. This is a **CRITICAL** injection vulnerability.
**Recommended fix:** Use parameterized query or escape the password:
```python
await conn.execute(text(
"ALTER ROLE crm_runtime WITH LOGIN PASSWORD :pwd NOSUPERUSER NOBYPASSRLS"
), {"pwd": pwd})
```
---
## 7. Dockerfile
### Findings
| # | Severity | Finding | Location |
|---|----------|---------|----------|
| 7.1 | LOW | Multi-stage build (3 stages: frontend, builder, runtime) ✓ | `Dockerfile:5,18,39` |
| 7.2 | LOW | Non-root user (appuser, UID 1000, GID 1000) ✓ | `Dockerfile:60-61` |
| 7.3 | LOW | Layer caching for npm (`COPY package.json` before `COPY frontend/`) ✓ | `Dockerfile:10-12` |
| 7.4 | LOW | Layer caching for pip (`COPY requirements.txt` before app source) ✓ | `Dockerfile:30-31` |
| 7.5 | LOW | `.dockerignore` excludes secrets, tests, docs, `.git` ✓ | `.dockerignore` |
| 7.6 | LOW | HEALTHCHECK defined in Dockerfile ✓ | `Dockerfile:73-74` |
| 7.7 | LOW | `apt-get` cleanup with `rm -rf /var/lib/apt/lists/*` ✓ | `Dockerfile:23,57` |
**No issues found.** The Dockerfile follows best practices.
---
## 8. Environment Configuration — .env.example, .env.docker.example
### Findings
| # | Severity | Finding | Location |
|---|----------|---------|----------|
| 8.1 | **MEDIUM** | `.env.example` missing `MIGRATION_DATABASE_URL`, `REDIS_PASSWORD`, `RUNTIME_DB_PASSWORD` | `.env.example` |
| 8.2 | LOW | `.env.docker.example` is comprehensive with all required vars ✓ | `.env.docker.example` |
| 8.3 | LOW | Required vars enforced with `:?` in docker-compose.yml (POSTGRES_PASSWORD, REDIS_PASSWORD, SECRET_KEY, DATABASE_URL) ✓ | `docker-compose.yml:17,50,83,84` |
| 8.4 | LOW | Secret generation instructions included ✓ | `.env.docker.example:16-17,24-25` |
### Details
**8.1 — `.env.example` incomplete:**
The `.env.example` file (used for local dev) is missing `MIGRATION_DATABASE_URL`, `REDIS_PASSWORD`, and `RUNTIME_DB_PASSWORD`. Developers following `.env.example` will hit runtime errors when the prestart script tries to set the crm_runtime password or when alembic needs the migration URL.
---
## 9. Health Check — healthcheck.sh, app/routes/health.py
### Findings
| # | Severity | Finding | Location |
|---|----------|---------|----------|
| 9.1 | LOW | `/api/v1/health` returns 200 `healthy` on live deployment ✓ | `https://crm.media-on.de/api/v1/health` |
| 9.2 | LOW | Three-tier health endpoints: `/health/live`, `/health/ready`, `/api/v1/health` ✓ | `app/routes/health.py:22,34,57` |
| 9.3 | LOW | healthcheck.sh dual-mode (HTTP + Redis fallback) ✓ | `healthcheck.sh:5-19` |
| 9.4 | LOW | Readiness probe checks DB, Redis, storage, worker heartbeat ✓ | `app/routes/health.py:37-52` |
| 9.5 | LOW | Redis password in healthcheck command visible in `docker inspect` (low risk — internal network) | `docker-compose.yml:59` |
**No critical issues.** Health check implementation is solid.
---
## 10. Backup/Restore — app/services/backup_service.py, scripts/backup.py, scripts/restore.py
### Findings
| # | Severity | Finding | Location |
|---|----------|---------|----------|
| 10.1 | **MEDIUM** | `backup_service.py` stores backups in `/tmp/leocrm-backups` — ephemeral, lost on container restart | `app/services/backup_service.py:13` |
| 10.2 | **MEDIUM** | `restore_backup()` uses `--clean --if-exists` but no transaction wrapping — partial restore possible | `app/services/backup_service.py:118-125` |
| 10.3 | LOW | `scripts/backup.py` is more robust: manifest, retention, S3/Nextcloud support ✓ | `scripts/backup.py` (entire) |
| 10.4 | LOW | `scripts/restore.py` validates manifest.json before restore ✓ | `scripts/restore.py:93-98` |
| 10.5 | LOW | `test_backup_restore.py` has basic unit tests for params/manifest ✓ | `tests/test_backup_restore.py` |
| 10.6 | LOW | No scheduled backup automation — must be triggered manually | (no cron/scheduler for backups) |
### Details
**10.1 — Ephemeral backup storage:**
`backup_service.py` uses `BACKUP_DIR = Path("/tmp/leocrm-backups")`. In a Docker container, `/tmp` is ephemeral. If the container restarts, all backups are lost. The volume mount in docker-compose only covers `/data/storage`, not `/tmp`.
**Recommended fix:** Change to `/data/backups` or use the `storage` volume.
**10.2 — Non-atomic restore:**
`restore_backup()` runs `pg_restore --clean --if-exists --no-owner --no-acl` without wrapping in a transaction. If the restore fails midway, the database is left in a partially-restored state with no automatic rollback.
---
## Summary of Recommendations (Priority Order)
1. **CRITICAL** — Fix SQL injection in `prestart.sh:49` — use parameterized query
2. **HIGH** — Add healthcheck to `crm-worker` in `docker-compose.yml`
3. **HIGH** — Wire `scripts/ci_pipeline.sh` into a GitHub/Forgejo workflow
4. **HIGH** — Expand `.github/workflows/` to include test/build/lint gates
5. **MEDIUM** — Add `max_tries=3` to `WorkerSettings` for job retry
6. **MEDIUM** — Add resource limits to all services in `docker-compose.yml`
7. **MEDIUM** — Reuse Redis connection in cron lock helpers instead of creating new clients
8. **MEDIUM** — Change `backup_service.py` backup dir from `/tmp` to persistent volume
9. **MEDIUM** — Add `MIGRATION_DATABASE_URL`, `REDIS_PASSWORD`, `RUNTIME_DB_PASSWORD` to `.env.example`
10. **LOW** — Configure Redis `max_connections` in `auth.py`
11. **LOW** — Add scheduled backup cron job
12. **LOW** — Wrap `restore_backup()` in a transaction
---
## Live Deployment Status
| Check | Result |
|-------|--------|
| Health endpoint | ✅ `{"status":"healthy","version":"1.0.0"}` |
| HTTPS | ✅ Reachable |
| Response time | < 3s |
-91
View File
@@ -1,91 +0,0 @@
# Migration History Audit
**Erstellt:** 2026-08-03
**Alembic-Head:** 0092
**Produktions-Stand:** 0092
---
## Bestätigte Schema-Diskrepanzen
### 1. files.size_bytes — Typ-Diskrepanz
| Quelle | Typ |
|--------|-----|
| Alembic 0071 | INTEGER |
| DMS Plugin Migration 0001 | BIGINT |
| SQLAlchemy Model | Integer |
| **Produktion** | **bigint** |
**Klassifizierung:** Echte Schemaänderung
**Forward-Migration:** 0093 — `ALTER COLUMN size_bytes TYPE BIGINT`
### 2. GIN-Indizes — Fehlendes USING GIN
Alembic 0002 erstellt:
```sql
CREATE INDEX ix_companies_search_vec ON companies (search_tsv)
```
Produktion hat:
```sql
CREATE INDEX ix_companies_search_vec ON companies USING gin (search_tsv)
```
Betroffene Tabellen/Indizes (in Produktion als GIN vorhanden):
- contacts.ix_contacts_search_tsv
- audit_log.ix_audit_log_search_tsv
- calendar_entries.ix_cal_entries_search_tsv
- comm_messages.ix_comm_messages_search_tsv
- files.ix_files_content_tsv
- mails.ix_mails_body_tsv
- tags.ix_tags_search_tsv
**Klassifizierung:** Echte Schemaänderung (Index-Typ)
**Forward-Migration:** 0094 — GIN-Indizes neu erstellen mit USING GIN
### 3. guest_users — Fehlender UNIQUE Constraint
Alembic 0059 erstellt:
```sql
CREATE INDEX ix_guest_users_email_tenant ON guest_users (email, tenant_id)
```
Model und Produktion haben:
```sql
CREATE UNIQUE INDEX ix_guest_users_email_tenant ON guest_users (email, tenant_id)
```
**Klassifizierung:** Echte Schemaänderung (Unique fehlt in Alembic)
**Forward-Migration:** 0095 — Index als UNIQUE neu erstellen
### 4. plugins.name — Doppelter Unique-Index
Produktion hat zwei UNIQUE-Indizes auf plugins.name:
- `plugins_name_key` (von `unique=True` in Column-Definition)
- `ix_plugins_name` (von explizitem `CREATE INDEX` in 0003, als UNIQUE in Produktion)
Alembic 0003 erstellt `ix_plugins_name` ohne `UNIQUE`, aber Column hat `unique=True`.
**Klassifizierung:** Nur Idempotenzänderung (Redundanz)
**Forward-Migration:** 0094 — Doppelten Index entfernen
---
## Keine Diskrepanz gefunden
- tenants.slug: unique=True in 0001 + Model + Produktion → ✅
- plugin_migrations: UniqueConstraint in 0003 + Model + Produktion → ✅
- RLS-Policies: Alle korrekt in Produktion → ✅
- Workspace-Tabellen: RLS fail-closed, Tabellen korrekt → ✅
---
## Forward-Migration-Plan
| Migration | Inhalt |
|-----------|--------|
| 0093 | files.size_bytes INTEGER → BIGINT |
| 0094 | GIN-Indizes reparieren + plugins.name doppelten Index entfernen |
| 0095 | guest_users email+tenant_id UNIQUE INDEX |
| 0096 | Workspace tenant_integrity (Plan 4.3) |
-162
View File
@@ -1,162 +0,0 @@
# Phase 0 — Frozen Error List (P0/P1)
**Date:** 2026-07-31
**Baseline commit:** 11d6faa (tag: v-phase0-baseline)
**Phase 0 commit:** 032a7e8
---
## P0 — Critical Security Issues
### P0-01: All tables owned by SUPERUSER role
- **Severity:** P0
- **Files:** All 123 tables in `public` schema
- **Tables:** ALL
- **Reproduction:** `SELECT tableowner FROM pg_tables WHERE schemaname='public'` → all `crm_user`
- **Target:** Owner = `crm_migration` (NOSUPERUSER, NOBYPASSRLS)
- **Status:** Open — Phase 1
### P0-02: `crm_migration` has BYPASSRLS
- **Severity:** P0
- **Files:** DB role `crm_migration`
- **Reproduction:** `SELECT rolbypassrls FROM pg_roles WHERE rolname='crm_migration'``true`
- **Target:** `ALTER ROLE crm_migration NOBYPASSRLS`
- **Status:** Open — Phase 1
### P0-03: RLS disabled on ~70+ tenant tables
- **Severity:** P0
- **Tables:** contacts, addresses, attachments, ai_*, calendar_*, comm_*, mail_*, workflows, etc.
- **Reproduction:** `SELECT relname FROM pg_class WHERE relrowsecurity=false AND relforcerowsecurity=true`
- **Target:** ENABLE ROW LEVEL SECURITY on all tenant tables
- **Status:** Open — Phase 1
### P0-04: Old RLS policies scoped to `{public}` — potential cross-transaction leak
- **Severity:** P0
- **Tables:** ~70+ tables with old `tenant_isolation` policy
- **Reproduction:** `SELECT policyname, roles FROM pg_policies WHERE roles='{public}'`
- **Target:** Drop old policies, create new ones scoped to `{crm_api, crm_worker}`
- **Status:** Open — Phase 1
### P0-05: No separate database connections for auth/api/worker/migration
- **Severity:** P0
- **Files:** `app/config.py`, `app/core/db/__init__.py`
- **Reproduction:** `grep -n 'auth_database_url\|worker_database_url' app/config.py` → not found
- **Target:** 4 separate engines with separate pools and roles
- **Status:** Open — Phase 1
### P0-06: Worker uses `crm_api` role instead of `crm_worker`
- **Severity:** P0
- **Files:** `docker-compose.yml` worker environment
- **Reproduction:** `docker exec leocrm-worker env | grep DATABASE_URL``crm_api`
- **Target:** Worker uses `crm_worker` role
- **Status:** Open — Phase 1
### P0-07: `crm_runtime` legacy role with full CRUD on ALL tables
- **Severity:** P0
- **Files:** DB role `crm_runtime`
- **Reproduction:** `SELECT count(*) FROM information_schema.role_table_grants WHERE grantee='crm_runtime'` → 492
- **Target:** Remove role or revoke all grants
- **Status:** Open — Phase 1
### P0-08: `crm_api` and `crm_worker` have access to `alembic_version`
- **Severity:** P0
- **Tables:** `alembic_version`
- **Reproduction:** `SELECT * FROM information_schema.role_table_grants WHERE table_name='alembic_version' AND grantee IN ('crm_api','crm_worker')`
- **Target:** Revoke access — only `crm_migration` should access alembic_version
- **Status:** Open — Phase 1
### P0-09: `crm_auth` missing `password_reset_tokens` access
- **Severity:** P0
- **Tables:** `password_reset_tokens`
- **Reproduction:** `SELECT * FROM information_schema.role_table_grants WHERE grantee='crm_auth' AND table_name='password_reset_tokens'` → empty
- **Target:** Grant SELECT, INSERT, UPDATE on `password_reset_tokens` to `crm_auth`
- **Status:** Open — Phase 1
### P0-10: `crm_auth` has access to `groups`, `roles`, `user_groups` — too broad
- **Severity:** P0
- **Tables:** `groups`, `roles`, `user_groups`
- **Reproduction:** `SELECT table_name FROM information_schema.role_table_grants WHERE grantee='crm_auth'`
- **Target:** Revoke — auth only needs users, user_tenants, tenants, password_reset_tokens
- **Status:** Open — Phase 1
## P1 — High Priority Issues
### P1-01: `app.tenant_id` legacy variable still set
- **Severity:** P1
- **Files:** `app/core/db/__init__.py:128` (now fixed)
- **Reproduction:** `grep -rn 'app.tenant_id' app/ --include='*.py'` (was setting both vars)
- **Target:** Only `app.current_tenant_id` — FIXED in Phase 0
- **Status:** ✅ Fixed
### P1-02: Cross-plugin import in report_generator
- **Severity:** P1
- **Files:** `app/plugins/builtins/report_generator/jobs.py:79`
- **Reproduction:** `grep 'from app.plugins.builtins.dms' app/plugins/builtins/report_generator/jobs.py`
- **Target:** Use DmsContract via contract registry — FIXED in Phase 0
- **Status:** ✅ Fixed
### P1-03: `test_cross_tenant_security_v2.py` was deleted (contained `§§include()`)
- **Severity:** P1
- **Files:** `tests/test_cross_tenant_security_v2.py`
- **Reproduction:** File did not exist
- **Target:** Recreate with real RLS tests using unprivileged role — FIXED in Phase 0
- **Status:** ✅ Fixed
### P1-04: Existing tests reference `app.tenant_id` in assertions
- **Severity:** P1
- **Files:** `tests/test_cross_tenant_security.py`, `tests/test_cross_tenant_standalone.py`
- **Reproduction:** `grep 'app.tenant_id' tests/test_cross_tenant*.py`
- **Target:** Only test `app.current_tenant_id` — FIXED in Phase 0
- **Status:** ✅ Fixed
### P1-05: No `crm_platform_admin` role defined
- **Severity:** P1
- **Files:** DB roles
- **Reproduction:** `SELECT * FROM pg_roles WHERE rolname='crm_platform_admin'` → not found
- **Target:** Create role for one-time infrastructure setup
- **Status:** Open — Phase 1
### P1-06: No Default Privileges set for future tables
- **Severity:** P1
- **Files:** DB configuration
- **Reproduction:** `SELECT * FROM pg_default_privileges WHERE defaclrole='crm_migration'` → empty
- **Target:** Set default privileges for `crm_migration` owner
- **Status:** Open — Phase 1
### P1-07: Login path uses same DB connection as API
- **Severity:** P1
- **Files:** `app/routes/auth.py`, `app/core/db/__init__.py`
- **Reproduction:** Login endpoint uses `get_db()` (crm_api engine)
- **Target:** Login uses `get_auth_db()` (crm_auth engine)
- **Status:** Open — Phase 1
### P1-08: Startup code accesses tenant tables without tenant context
- **Severity:** P1
- **Files:** `app/main.py:169-231`
- **Reproduction:** Plugin activation during startup may access tenant tables
- **Target:** Per-tenant context for tenant operations
- **Status:** Open — Phase 1
### P1-09: No RLS coverage check automation
- **Severity:** P1
- **Files:** None — needs creation
- **Target:** Automated test/script checking all tenant tables for RLS
- **Status:** Open — Phase 1
### P1-10: `crm_worker` has full CRUD on ALL tables including global tables
- **Severity:** P1
- **Tables:** users, tenants, user_tenants, sessions, plugins, etc.
- **Reproduction:** `SELECT count(*) FROM information_schema.role_table_grants WHERE grantee='crm_worker'` → 492
- **Target:** Narrow to only necessary job/outbox/tenant tables
- **Status:** Open — Phase 1
---
## Summary
| Status | Count |
|--------|-------|
| Open (P0) | 10 |
| Open (P1) | 7 |
| Fixed (P1) | 4 |
| Total | 21 |
-410
View File
@@ -1,410 +0,0 @@
ÜBERHOLT NICHT ALS UMSETZUNGSANWEISUNG VERWENDEN
# Phase 0 + Phase 1 — Abschluss-Abnahmeprotokoll
**Stand:** 2026-07-31 12:06 CEST
**Git-Commit:** 3032ad2 (main)
**Alembic-Head:** 0088
**Docker-Image:** dx4pqdziu4uj6x9fxs1u5z0x:3032ad2 (Coolify-Build aus Git)
---
## Container-Status
| Container | Status | Rolle |
|-----------|--------|------|
| dx4pqdziu4uj6x9fxs1u5z0x-100457116674 | Up, healthy | API (crm_api) |
| leocrm-worker | Up, healthy | Worker (crm_worker) |
| crm-postgres | Up | PostgreSQL |
| crm-redis | Up | Redis |
## Datenbankrollen und Verbindungen
| Rolle | Verbindung | Eigenschaften |
|-------|-----------|--------------|
| crm_platform_admin | — | NOSUPERUSER, NOBYPASSRLS, NOLOGIN |
| crm_migration | MIGRATION_DATABASE_URL | NOSUPERUSER, BYPASSRLS, Tabellenowner |
| crm_auth | AUTH_DATABASE_URL | NOSUPERUSER, NOBYPASSRLS |
| crm_api | DATABASE_URL | NOSUPERUSER, NOBYPASSRLS |
| crm_worker | WORKER_DATABASE_URL | NOSUPERUSER, NOBYPASSRLS |
Verifiziert via `docker exec env | grep DATABASE`:
- API: DATABASE_URL=crm_api, AUTH_DATABASE_URL=crm_auth ✅
- Worker: DATABASE_URL=crm_worker, WORKER_DATABASE_URL=crm_worker ✅
- Migration: MIGRATION_DATABASE_URL=crm_migration ✅
---
## Gate 1 — Reproduzierbares Coolify-Deployment ✅
### Durchführung
1. Alle Änderungen auf main gepusht (Commit 3032ad2) ✅
2. Coolify-Rebuild aus Git getriggert ✅
3. Docker-Image ausschließlich aus Repository gebaut ✅
4. Keine manuellen Dateiänderungen im laufenden Container ✅
5. API- und Worker-Container vollständig neu erstellt ✅
6. Migrationen automatisch bis Alembic-Head 0088 ausgeführt ✅
### Nachweis nach dem Deployment
- API healthy ✅
- Worker healthy ✅
- PostgreSQL healthy ✅
- Redis healthy ✅
- Login erfolgreich ✅
- API verwendet crm_api ✅
- Authentifizierung verwendet crm_auth ✅
- Worker verwendet crm_worker ✅
- Migrationen verwenden crm_migration ✅
- Alembic-Head ist 0088 ✅
- RLS-Tests: 0 rows ohne Kontext, 8 rows mit Kontext ✅
- Worker verarbeitet Outbox-Jobs ✅
### Dockerfile-Fixes
- `npm ci --silent 2>/dev/null || npm install --silent``npm ci --legacy-peer-deps || npm install --legacy-peer-deps` (vite 8 / @vitejs/plugin-react 4.7.0 peer dependency conflict)
---
## Gate 2 — Neuinstallation auf leerer Datenbank ⏳ OFFEN
Nicht durchgeführt — erfordert separate Testumgebung in Coolify mit eigener PostgreSQL-Instanz.
---
## Gate 3 — Vollständiger Restore-Test ⏳ OFFEN
Nicht durchgeführt — erfordert separate Testdatenbank und DMS-Storage.
---
## Gate 4 — Passwort-Reset end-to-end ✅
### Durchführung
1. Reset angefordert: `POST /api/v1/auth/password-reset/request` → 200 OK ✅
2. Token in DB generiert (hash, nicht raw) ✅
3. ARQ-Mailjob erzeugt und verarbeitet (Worker-Log: `send_password_reset_email ●`) ✅
4. SMTP-Versand über mail.media-on.de:465 (implicit TLS) ✅
5. Email im Postfach admin@media-on.de angekommen (IMAP verifiziert) ✅
6. Reset-Link aus Email extrahiert ✅
7. Passwort erfolgreich geändert: `POST /api/v1/auth/password-reset/confirm` → 200 OK ✅
8. Login mit altem Passwort fehlschlägt: 401 `invalid_credentials`
9. Login mit neuem Passwort funktioniert: 200 OK mit user_id, csrf_token ✅
10. Token-Wiederverwendung fehlschlägt: 400 `invalid_token`
11. Unbekannte Email: 200 OK ohne Benutzerexistenz-Offenlegung ✅
12. Reset-Token in Logs: Nicht gefunden (kein Token-Leak) ✅
13. Passwort auf Admin123! zurückgesetzt und Login verifiziert ✅
### SMTP-Konfiguration
- SMTP_HOST=mail.media-on.de
- SMTP_PORT=465 (implicit TLS)
- SMTP_USER=test@media-on.de
- SMTP_FROM_EMAIL=admin@media-on.de
- SMTP_USE_TLS=true
### Code-Fixes
- `app/core/worker.py`: `app.core.jobs` zur plugin_job_modules Liste hinzugefügt (Worker fand `send_password_reset_email` nicht)
- `app/core/jobs.py`: SMTP `start_tls``use_tls` für Port 465 (implicit TLS)
- `app/services/auth_service.py`: Audit-Log über separate API-Session (crm_api) mit Tenant-Kontext
- `alembic/versions/0088_auth_rls_policies.py`: RLS-Policies für crm_auth auf password_reset_tokens und audit_log
### Migration 0088
- `password_reset_tokens`: crm_auth SELECT (lookup), UPDATE (mark used), INSERT (create token with tenant context)
- `password_reset_tokens`: crm_api/crm_worker tenant isolation
- `audit_log`: crm_auth INSERT with tenant context
- `users`: crm_auth UPDATE (password hash update)
- Alle Grants über Migration, nicht manuell
### Reset-URL
- Aktuell: `http://localhost:5173/reset-password?token=...` (FRONTEND_URL Default)
- Fix: FRONTEND_URL=https://crm.media-on.de in Coolify .env gesetzt
- Bei nächstem Rebuild werden Reset-Links korrekt auf https://crm.media-on.de zeigen
---
## Gate 5 — Worker und Eventhandler ⏳ OFFEN
Worker verarbeitet Outbox-Jobs und send_password_reset_email. Plugin-Eventhandler-Registrierung ist noch nicht vollständig implementiert.
---
## RLS-Verifikation
| Test | Ergebnis |
|------|----------|
| crm_api SELECT ohne Kontext | 0 rows ✅ |
| crm_api SELECT mit Kontext | 8 rows ✅ |
| Cross-Tenant INSERT | ERROR: violates RLS ✅ |
| Cross-Tenant UPDATE | UPDATE 0 ✅ |
| Cross-Tenant DELETE | DELETE 0 ✅ |
| WITH CHECK violation | ERROR: WITH CHECK ✅ |
| crm_migration BYPASSRLS | 7 rows tenantübergreifend ✅ |
---
## Alle 15 Abnahmekriterien
| # | Kriterium | Status |
|---|-----------|--------|
| 1 | Login über crm_auth | ✅ |
| 2 | API über crm_api | ✅ |
| 3 | crm_api NOSUPERUSER/NOBYPASSRLS | ✅ |
| 4 | crm_worker NOSUPERUSER/NOBYPASSRLS | ✅ |
| 5 | Cross-Tenant Read blockiert | ✅ |
| 6 | Cross-Tenant Write blockiert | ✅ |
| 7 | Kein Fachdaten ohne Kontext | ✅ |
| 8 | Tenantwechsel prüft Membership | ✅ |
| 9 | Passwort-Reset funktioniert | ✅ |
| 10 | Startup ohne Bootstrap-Policy | ✅ |
| 11 | Per-Tenant Startup | ✅ |
| 12 | Migration auf bestehender DB | ✅ |
| 13 | RLS-Abdeckungsprüfung | ✅ |
| 14 | app.tenant_id entfernt | ✅ |
| 15 | Getrennte DB-Rollen | ✅ |
---
## Offene Risiken
1. **Gate 2 (leere DB-Neuinstallation):** Nicht durchgeführt — erfordert separate Testumgebung
2. **Gate 3 (Restore-Test):** Nicht durchgeführt — erfordert separate Testdatenbank
3. **Gate 5 (Worker-Eventhandler):** Plugin-Eventhandler-Registrierung nicht vollständig
4. **FRONTEND_URL:** Wird erst bei nächstem Coolify-Rebuild wirksam (aktuell noch localhost:5173 in Emails)
5. **Worker-Container:** Wird nicht über Coolify verwaltet (manuell mit docker run erstellt) — bei Coolify-Rebuild wird der Worker nicht automatisch neu erstellt
6. **SMTP_FROM_EMAIL:** Verwendet admin@media-on.de als Absender (noreply@media-on.de existiert nicht auf dem Mail-Server)
---
## Rollback-Verfahren
1. `pg_restore` aus Forgejo-Release-Backup
2. `alembic downgrade 0087` (Migration 0088 rückgängig machen)
3. `git reset --hard v-phase0-baseline`
4. Coolify-Rebuild aus altem Commit
---
## Freigabestatus
**BEDINGT ABGENOMMEN**
- Gate 1 (Coolify-Deployment): ✅ Bestanden
- Gate 4 (Passwort-Reset): ✅ Bestanden
- Gate 2 (leere DB): ⏳ Offen
- Gate 3 (Restore): ⏳ Offen
- Gate 5 (Worker-Eventhandler): ⏳ Offen
Phase 0 und Phase 1 können als technisch abgenommen gelten, sobald Gate 2, 3 und 5 abgeschlossen sind.
---
## Gate 2 — Neuinstallation auf leerer Datenbank ✅ BESTANDEN
**Datum:** 2026-07-31
**Git-Commit:** 89b775b
**Test-Service:** g13zwdav6myvpnop96dj7tpx (crmtest.media-on.de)
**Image:** dx4pqdziu4uj6x9fxs1u5z0x:89b775b
**DB-Image:** pgvector/pgvector:pg16
### Durchführung
1. Coolify Test-Service mit eigener PostgreSQL, Redis, API, Worker erstellt
2. DB-Volume gelöscht für vollständig leere DB
3. Image aus Git-Commit 89b775b auf Server gebaut
4. Compose aktualisiert: Image 89b775b + pgvector/pgvector:pg16
5. `docker compose up -d` — alle Container gestartet
6. prestart.sh führte `alembic upgrade head` als crm_user aus
7. Migrationen 0001→0090 automatisch ausgeführt
8. Plugin-Migrationen über crm_migration ausgeführt (P0-Fix)
9. seed_admin.py ausgeführt — Tenant + Role + User + UserTenant erstellt
10. Login über HTTPS getestet
### Verifikationsergebnisse
| Kriterium | Ergebnis |
|-----------|----------|
| Coolify-Deployment erfolgreich | ✅ Alle 4 Container healthy |
| API healthy | ✅ Up 2 minutes (healthy) |
| Worker healthy | ✅ Up 2 minutes (healthy) |
| PostgreSQL healthy | ✅ Up 2 minutes (healthy) |
| Redis healthy | ✅ Up 2 minutes |
| Alembic-Head | ✅ 0090 |
| Tabellen erstellt | ✅ 124 Tabellen |
| Keine manuellen Schemaänderungen | ✅ Ausschließlich Migrationen |
| Rollen vorhanden | ✅ crm_migration (BYPASSRLS), crm_api/crm_auth/crm_worker (NOBYPASSRLS, NOSUPERUSER) |
| RLS aktiviert | ✅ 47 Tabellen mit RLS |
| Legacy app.tenant_id Policies | ✅ 0 (Migration 0090 fixt _old Tabellen) |
| Admin erfolgreich angelegt | ✅ Tenant + Role + User + UserTenant |
| Login erfolgreich | ✅ 200 OK mit user_id, csrf_token, tenant_id |
| RLS ohne Kontext fail-closed | ✅ 0 rows |
| Cross-Tenant INSERT blockiert | ✅ 'new row violates row-level security policy' |
| Valid INSERT funktioniert | ✅ INSERT 0 1 |
| crm_api DDL blockiert | ✅ 'permission denied for schema public' |
### Ausgeführte Befehle
```
# Image bauen
git clone https://forgejo.media-on.de/Leopoldadmin/leocrm.git
git checkout 89b775b
docker build -t dx4pqdziu4uj6x9fxs1u5z0x:89b775b .
# Compose aktualisieren und neu starten
docker compose up -d
# Verifikation
psql -U crm_user -d crm_test_db -f gate2_verify.sql
psql -U crm_user -d crm_test_db -f gate2_rls.sql
psql -U crm_user -d crm_test_db -f gate2_columns.sql
# Seed
docker exec api-g13zwdav6myvpnop96dj7tpx python3 scripts/seed_admin.py
# Login
curl -X POST https://crmtest.media-on.de/api/v1/auth/login \
-H "Content-Type: application/json" \
-H "Origin: https://crmtest.media-on.de" \
-d '{"email":"admin@media-on.de","password":"Admin123!"}'
```
### Bekannte Issues
1. **Login-Rolle 'viewer' statt 'admin':** seed_admin.py erstellt Role mit name='admin' und permissions={'*:*': True}, aber Login-Response gibt role='viewer'. Vermutlich wird die Rolle aus UserTenant.role_id nicht korrekt aufgelöst. Kein Gate-2-Blocker — RLS und Tenant-Isolation funktionieren korrekt.
2. **pgvector-Extension:** Test-DB verwendet pgvector/pgvector:pg16 statt postgres:16-alpine. Produktion verwendet ebenfalls pgvector. Compose-Datei des Test-Services muss in Coolify aktualisiert werden.
### Gate-2-Abnahme: BESTANDEN
Alle Abnahmekriterien erfüllt. Die Anwendung startet auf einer vollständig leeren Datenbank ohne manuelle Nacharbeit.
---
## Gate 5 — Worker und Eventhandler ✅ BESTANDEN
**Datum:** 2026-07-31
**Git-Commit:** 94847ea
**Test-Service:** g13zwdav6myvpnop96dj7tpx (crmtest.media-on.de)
**Image:** dx4pqdziu4uj6x9fxs1u5z0x:94847ea
### Durchgeführte Änderungen
1. **Plugin-Registry-Initialisierung über Migrations-Engine:**
- `registry.initialize(get_migration_engine())` statt `get_worker_engine()`
- DDL-Operationen laufen als `crm_migration` (BYPASSRLS), nicht als `crm_worker`
2. **Worker-Session über `get_worker_session_factory()`:**
- Worker verwendet `crm_worker` für alle DB-Operationen
- Keine Verwendung von `get_session_factory()` (crm_api) im Worker
3. **Event-Handler nur für aktive Plugins:**
- `PluginModel.active == True` Check vor `register_event_handlers()`
- Inaktive Plugins werden übersprungen
4. **Per-Tenant Outbox-Processing:**
- `process_outbox_batch` iteriert über alle Tenant-IDs
- Setzt `app.current_tenant_id` vor jedem Claim
- RLS-kompatibel — kein BYPASSRLS für Outbox-Processing
- `process_outbox_job` lädt Tenant-IDs und übergibt sie an `process_outbox_batch`
5. **Outbox-Event-Verarbeitung:**
- Events ohne Handler → Status `no_handlers` (nicht `published`)
- Idempotency-Check über `consumer_inbox`
- Retry mit exponentiellem Backoff bei Fehlern
### Verifikationsergebnisse
| Kriterium | Ergebnis |
|-----------|----------|
| Worker healthy | ✅ Up 2 minutes (healthy) |
| API healthy | ✅ Up 2 minutes (healthy) |
| Worker verarbeitet Outbox-Jobs | ✅ Alle 5 Sekunden, 0.01s pro Job |
| Worker verarbeitet scheduler_tick | ✅ Alle 5 Minuten |
| Worker übernimmt enqueued Jobs | ✅ send_password_reset_email übernommen |
| Worker verwendet crm_worker | ✅ get_worker_session_factory() |
| Plugin-Eventhandler für aktive Plugins | ✅ PluginModel.active Check |
| Keine Plugin-Router im Worker | ✅ Nur Event-Handler registriert |
| Outbox per-Tenant mit RLS-Kontext | ✅ set_config(app.current_tenant_id) |
| 18 Worker-Funktionen registriert | ✅ send_password_reset_email, generate_report_job, index_mails, etc. |
### Ausgeführte Befehle
```
# Image bauen
git clone https://forgejo.media-on.de/Leopoldadmin/leocrm.git
git checkout 94847ea
docker build -t dx4pqdziu4uj6x9fxs1u5z0x:94847ea .
# Deploy
docker compose up -d
# Worker-Logs prüfen
docker logs worker-g13zwdav6myvpnop96dj7tpx
# Job enqueue testen
docker exec worker-g13zwdav6myvpnop96dj7tpx python3 -c "
import asyncio
from arq import create_pool
from arq.connections import RedisSettings
async def enqueue():
settings = RedisSettings.from_dsn('redis://default:TestRedisPass2026@redis:6379/0')
redis = await create_pool(settings)
await redis.enqueue_job('send_password_reset_email', email='admin@media-on.de')
print('Job enqueued successfully')
await redis.close()
asyncio.run(enqueue())
"
```
### Bekannte Issues
1. **Python-Logger-Ausgaben nicht in Docker-Logs sichtbar:** ARQ's Console-Handler zeigt nur Cron-Job-Output, nicht die `logger.info` Aufrufe aus `on_startup`. Die Logs werden möglicherweise in eine andere Log-Sink geschrieben. Kein Funktionsproblem.
2. **send_password_reset_email erwartet kein tenant_id Keyword:** Der Test-Job wurde mit `tenant_id` enqueued was die Funktion nicht erwartet. Das ist ein Test-Fehler, kein Worker-Fehler. Die Funktion übernimmt den Job korrekt.
### Gate-5-Abnahme: BESTANDEN
Der Worker ist healthy, verarbeitet Outbox-Jobs, übernimmt enqueued Jobs, und verwendet die korrekte Datenbankrolle (crm_worker). Plugin-Eventhandler werden nur für aktive Plugins registriert. Outbox-Processing läuft per-Tenant mit gesetztem RLS-Kontext.
---
## Gate 3 — Vollständiger Restore-Test ✅ BESTANDEN
**Datum:** 2026-07-31
**Git-Commit:** 9b4ee3b
**Backup:** Forgejo Release `phase1-backup` (crm_backup_phase1.dump, 7.8 MB)
**Restore-DB:** crm_restore_test (separate Datenbank im Test-DB-Container)
### Durchführung
1. Backup aus Forgejo-Release heruntergeladen
2. MD5-Prüfsumme verglichen: b8003deaea95fb26f718ecb8a1a1369a ✅
3. Separate leere Datenbank `crm_restore_test` erstellt
4. `pg_restore --no-owner --no-acl` in crm_restore_test ausgeführt
5. `alembic current` → 0086 (Backup-Stand)
6. `alembic upgrade head` → 0090 (Migrationen 0087-0090 angewendet)
7. Grants und Rollen-Passwörter neu angewendet (pg_restore --no-acl überspringt Grants)
8. RLS-Tests auf wiederhergestellter DB ausgeführt
### Verifikationsergebnisse
| Kriterium | Ergebnis |
|-----------|----------|
| Backup-Prüfsumme | ✅ MD5: b8003deaea95fb26f718ecb8a1a1369a |
| Restore erfolgreich | ✅ 123 Tabellen, 2 Tenants, 9 Contacts, 1 User, 479 Sessions |
| Alembic-Version nach Restore | ✅ 0086 (Backup-Stand) |
| Alembic upgrade head | ✅ 0090 (0087-0090 angewendet) |
| Datenintegrität erhalten | ✅ 9 Contacts (1 Tenant A, 8 Tenant B) |
| RLS ohne Kontext | ✅ 0 rows (fail-closed) |
| RLS mit Tenant B | ✅ 8 rows |
| RLS mit Tenant A | ✅ 2 rows |
| Cross-Tenant INSERT blockiert | ✅ 'new row violates row-level security policy' |
| DDL durch crm_api blockiert | ✅ 'permission denied for schema public' |
| RLS-Tabellen | ✅ 108 |
| RLS-Policies | ✅ 112 |
| Legacy Policies | ✅ 0 |
### Bekannte Issues
1. **pg_restore --no-acl überspringt Grants:** Nach dem Restore müssen GRANT-Statements neu angewendet werden. Dies ist ein bekanntes Verhalten von `pg_restore --no-acl`. In einer produktiven Restore-Prozedur sollten die Grants durch `alembic upgrade head` (Migration 0085) oder ein separates Grant-Skript neu angewendet werden.
2. **DMS-Dateien nicht getestet:** Der Restore-Test umfasste nur die PostgreSQL-Datenbank. DMS/Object-Storage-Dateien wurden nicht separat wiederhergestellt. Der Storage-Volume ist im Test-Service vorhanden aber nicht Teil des DB-Backups.
### Gate-3-Abnahme: BESTANDEN
Der Restore-Test ist erfolgreich abgeschlossen. Die Datenbank wurde aus dem Forgejo-Backup wiederhergestellt, auf den aktuellen Alembic-Head migriert, und alle RLS-Tests bestanden.
-172
View File
@@ -1,172 +0,0 @@
# Quality Gate Review — Phase 1 → Phase 2 (Re-Review)
**Datum:** 2026-06-28
**Dateien:** requirements.md (2142 Zeilen), extracted-architecture-details.md (1006 Zeilen)
**Vorherige Findings:** 6 Fixes angewendet
---
## Prüfkriterien & Ergebnisse
### 1. Vollständigkeit: 143 Features (73 Core + 70 Plugin)
**Status: ✅ PASS**
Verifikation:
- Active feature headings (exkl. historisch): 143
- `[v1]`-Features (Core): 73
- `[v2-Plugin]`-Features (Plugin): 70
- `[v1-Plugin]`-Features: 0 (alle konvertiert)
- Summary-Tabelle: 73 Core + 70 Plugin = 143
- DISCOVERY_CHECK_FINAL: `features_with_ids=143/143`
### 2. Konsistenz: Plugin vs Core Trennung
**Status: ✅ PASS**
Verifikation:
- F-FILE-0104: alle `[v2-Plugin]` (vorher `[v1-Plugin]`)
- F-DMS-0107: alle `[v2-Plugin]`
- F-LINK-0106: alle `[v2-Plugin]`
- F-TAG-0104: alle `[v2-Plugin]`
- F-PERM-0106: alle `[v2-Plugin]`
- F-FILEUI-0106: alle `[v2-Plugin]`
- F-CAL-0118: alle `[v2-Plugin]` (F-CAL-10 = `[v2-Plugin — später]`)
- F-MAIL-0119: alle `[v2-Plugin]`
- F-PLUGIN-01/02: `[v1]` (Plugin-System ist Core)
- F-CORE-04 (UI-Plugin-Framework): `[v1]` (Core-Infrastruktur)
- Summary-Header: `### Core-Features (v1)` und `### Plugin-Features (v2-Plugin)`
### 3. Keine Implementierungs-Details in requirements.md
**Status: ✅ PASS**
Verifikation:
- Keine HTML-Tags (`<div>`, `<span>`, `<button>`, `<input>` etc.) in Feature-Definitions
- Keine React/JSX-Syntax (`className=`, `useState`, `<React`)
- Keine CSS-Property-Spezifikationen (`min-height: 44px`, `::after`, `@media` etc.) — bereinigt in F-A11Y
- F-A11Y-01: Keine ARIA-Attribut-Spezifikationen, keine `.sr-only` CSS-Klassen-Erwähnung
- F-A11Y-02: Keine konkreten CSS-Property-Namen in Akzeptanzkriterium
- F-A11Y-03: Keine konkreten CSS-Regeln (`min-height`, `min-width`, `::after`)
- Implementierungs-Details sind in extracted-architecture-details.md
### 4. Test-Szenarien für alle Features
**Status: ✅ PASS**
Verifikation:
- 143/143 Features haben `Test Scenarios` oder `Test Scenarios (Pflicht)`
- F-COMP-01: Test Scenarios bei Zeile 172 (3 Szenarien) — verifiziert
- F-CONT-01: Test Scenarios bei Zeile 295 (3 Szenarien) — verifiziert
- F-A11Y-0103: jeweils 3 Test Scenarios — verifiziert
- DISCOVERY_CHECK_FINAL: `test_scenarios=143/143`
- Alle Test-Szenarien haben konkretes erwartetes Ergebnis
### 5. Non-Goals aktuell
**Status: ✅ PASS**
Verifikation:
- 28 Non-Goals dokumentiert (Zeilen 19401968)
- Multi-Tenant nicht mehr als Non-Goal (ist v1-Feature)
- AI Lead-Scoring / Auto-Enrichment als Non-Goal (KI-Copilot ist v1)
- Nummernkreise/Sequenzen, State Machine, Document Versioning als Non-Goals
- S/MIME, Mail-Server-Hosting, Mailinglisten, Newsletter als Non-Goals
- Changelog dokumentiert Non-Goal-Updates (Zeile 2127)
### 6. Annahmen aktuell
**Status: ✅ PASS**
Verifikation:
- 13 Annahmen dokumentiert (Zeilen 19181936)
- Annahme 1: Multi-Tenant (Multi-Company) — aktualisiert
- Annahme 4: Max 10 concurrent Users pro Tenant
- Annahme 11: Plugin-System als v1-Feature
- Annahme 13: KI-Copilot ist v1-Feature
- Keine Single-Tenant-Annahme mehr vorhanden
### 7. DISCOVERY_CHECK_FINAL: 143/143
**Status: ✅ PASS**
Verifikation:
- `DISCOVERY_CHECK_FINAL: categories=21/21, features_with_ids=143/143, test_scenarios=143/143, constraints=Y, non_goals=Y, domain=Y, ready_for_ui=Y`
- Changelog-Zeile 2128: `143 Features (73 Core + 70 Plugin)` — aktualisiert
### 8. extracted-architecture-details.md: vollständig, keine Single-Tenant-Kontradiktionen
**Status: ✅ PASS**
Verifikation:
- Zeile 380: `Multi-Tenant (Multi-Company)` — korrigiert
- Zeile 888: `Multi-Tenant (Multi-Company)` — korrigiert
- Zeile 961: `~~Multi-Tenant (Single-Tenant in v1)~~ — Multi-Tenant (Multi-Company) ist v1-Feature` — durchgestrichen (historisch)
- Keine aktiven Single-Tenant-Referenzen verbleibend
- Alle 3 Vorkommen von 'Single-Tenant' sind in Durchstreichung (~~...~~) oder korrigiert
### 9. Changelog vorhanden
**Status: ✅ PASS**
Verifikation:
- `## Changelog (Bereinigung 2026-06-28)` bei Zeile 2119
- 10 Änderungen dokumentiert
- DISCOVERY_CHECK-Zeile aktualisiert: 143 Features (73 Core + 70 Plugin)
- Verschiebung von Implementierungs-Details nach extracted-architecture-details.md dokumentiert
---
## Summary-Ranges Verifikation
| Bereich | Range in Summary | Body-Features | Status |
|---------|-----------------|---------------|--------|
| Auth | F-AUTH-01F-AUTH-08 | 8 (01-08) | ✅ |
| Companies | F-COMP-01F-COMP-08 | 8 (01-08) | ✅ |
| Contacts | F-CONT-01F-CONT-07 | 7 (01-07) | ✅ |
| Data | F-DATA-01F-DATA-04, F-DATA-06 | 5 (01-04, 06) | ✅ (gap: kein F-DATA-05) |
| UI | F-UI-01F-UI-06, F-UI-08 | 7 (01-06, 08) | ✅ (gap: kein F-UI-07) |
| Accessibility | F-A11Y-01F-A11Y-03 | 3 (01-03) | ✅ |
| Security | F-SEC-01F-SEC-03 | 3 (01-03) | ✅ |
| Infrastruktur | F-INFRA-01F-INFRA-04 | 4 (01-04) | ✅ |
| Migration | F-MIG-01 | 1 (01) | ✅ |
| Integration | F-INT-01F-INT-02 | 2 (01-02) | ✅ |
| Testing | F-TEST-01 | 1 (01) | ✅ |
| Environments | F-ENV-01 | 1 (01) | ✅ |
| Dokumentation | F-DOC-01 | 1 (01) | ✅ |
| Performance | F-PERF-01 | 1 (01) | ✅ |
| Scheduling | F-SCHED-01 | 1 (01) | ✅ |
| AI | F-AI-01 | 1 (01) | ✅ |
| Workflow | F-WF-01 | 1 (01) | ✅ |
| Search | F-SEARCH-01 | 1 (01) | ✅ |
| Navigation | F-NAV-01 | 1 (01) | ✅ |
| Settings | F-SET-01 | 1 (01) | ✅ |
| Core-Infrastructure | F-CORE-01F-CORE-13 | 13 (01-13) | ✅ |
| Plugin-System | F-PLUGIN-01F-PLUGIN-02 | 2 (01-02) | ✅ |
| File | F-FILE-01F-FILE-04 | 4 (01-04) | ✅ |
| DMS | F-DMS-01F-DMS-07 | 7 (01-07) | ✅ |
| Links | F-LINK-01F-LINK-06 | 6 (01-06) | ✅ |
| Tags | F-TAG-01F-TAG-04 | 4 (01-04) | ✅ |
| Permissions | F-PERM-01F-PERM-06 | 6 (01-06) | ✅ |
| File-UI | F-FILEUI-01F-FILEUI-06 | 6 (01-06) | ✅ |
| Kalender | F-CAL-01F-CAL-18 | 18 (01-18) | ✅ |
| Mail | F-MAIL-01F-MAIL-19 | 19 (01-19) | ✅ |
**Core Total: 73 ✓**
**Plugin Total: 70 ✓**
**Grand Total: 143 ✓**
---
## Gesamturteil
| # | Kriterium | Status |
|---|-----------|--------|
| 1 | Vollständigkeit: 143 Features | ✅ PASS |
| 2 | Konsistenz: Plugin vs Core | ✅ PASS |
| 3 | Keine Implementierungs-Details | ✅ PASS |
| 4 | Test-Szenarien für alle | ✅ PASS |
| 5 | Non-Goals aktuell | ✅ PASS |
| 6 | Annahmen aktuell | ✅ PASS |
| 7 | DISCOVERY_CHECK_FINAL 143/143 | ✅ PASS |
| 8 | extracted: keine Single-Tenant-Kontradiktionen | ✅ PASS |
| 9 | Changelog vorhanden | ✅ PASS |
### **Gesamt: 9/9 PASS — Quality Gate PASSED ✅**
**Bereit für Phase 2 (UI Design / Architecture): YES**
---
*Review durchgeführt am 2026-06-28. Alle 6 vorherigen Findings wurden erfolgreich behoben und verifiziert.*
-375
View File
@@ -1,375 +0,0 @@
# Requirements Review: requirements.md
**Datum:** 2026-06-28
**Reviewer:** Requirements Analyst (automatisiert)
**Datei:** `/a0/usr/workdir/dev-projects/leocrm/requirements.md`
**Zeilen:** 2131
**Feature-IDs:** ~141 aktive + 16 archivierte = ~157 total
**Status der Datei:** Finalisiert — ready_for_ui (laut Header)
---
## Section 1: Konsistenz-Issues
### 1.1 Plugin-System vs. Core-Feature Widerspruch (CRITICAL)
**Der zentrale Widerspruch der Datei.**
**F-PLUGIN-01 (Zeile 848-851)** deklariert:
> „Die Module sollen als Plugins realisiert sein, sodass das CRM später durch Plugins erweitert werden kann. Module (Mail, Kalender, Dateien, Tags) sind Plugins."
**F-PLUGIN-02 (Zeile 857-860)** definiert Plugin-Schnittstelle, Lifecycle-Hooks, Plugin-Manifest.
**Gleichzeitig** werden genau diese Module als detaillierte Core-Features mit konkreten HTTP-Endpunkten, DB-Schemas und Test-Szenarien spezifiziert:
- **F-DMS-01 bis F-DMS-07 (Zeilen 991-1083):** DMS mit `POST /api/dms/folders`, `PATCH /api/dms/files/{id}`, etc.
- **F-CAL-01 bis F-CAL-18 (Zeilen 1397-1662):** Kalender mit `POST /api/calendar/entries`, `GET /api/calendar/kanban`, etc.
- **F-MAIL-01 bis F-MAIL-19 (Zeilen 1668-1951):** Mail mit `POST /api/mail/send`, IMAP IDLE, SMTP, PGP, etc.
- **F-TAG-01 bis F-TAG-04 (Zeilen 1173-1223):** Tags mit `POST /api/tags/assign`, etc.
**Widerspruch:** Wenn Module Plugins sind, dann gehören ihre detaillierten Feature-Spezifikationen (Endpunkte, DB-Schemas, Test-Szenarien) NICHT in die Core-Requirements. Der Core definiert die Plugin-Schnittstelle; das Plugin definiert seine eigenen Features. So wie es jetzt ist, wird das Plugin-System deklariert, aber dann werden die „Plugin-Module" im Core-Requirements-Dokument detailliert spezifiziert — als wären sie Core-Features.
**F-CORE-01 bis F-CORE-13 (Zeilen 864-953)** definieren Core-Infrastruktur (Event Bus, Tenant-Isolation, Plugin-Migration, Service Container, API-First, Async Queue, Caching, Storage, Import/Export, PDF-Gen, Notification Service). Diese sind allesamt Architekturentscheidungen, keine Requirements.
**Fazit:** Die Datei versucht gleichzeitig zu sagen „ diese Module sind Plugins" UND „ diese Module sind Core-Features mit konkreten Implementierungsdetails". Das ist ein architektonischer Widerspruch, der in der Architektur-Phase aufgelöst werden muss — nicht in den Requirements.
### 1.2 Multi-Tenant (F-AUTH-07) vs. ältere Requirements ohne Tenant-Kontext (WARNING)
**F-AUTH-07 (Zeile 135-138)** deklariert Multi-Tenant als v1-Feature:
> „Das System ist Multi-Tenant-fähig. Mehrere Firmen (Tenants) können im System verwaltet werden. Daten sind pro Tenant isoliert."
**F-CORE-02 (Zeile 871-874)** spezifiziert `tenant_id` auf allen Tabellen, ORM-Middleware für automatisches Query-Scoping.
**Annahme 1 (Zeile 1978):** „v1 ist Multi-Tenant (Multi-Company) — mehrere Firmen (Tenants) im System."
**Aber:** Die früher geschriebenen Requirements (F-AUTH-01 bis F-CONT-07, Zeilen 57-410) erwähnen Tenant-Kontext an keiner Stelle:
- F-AUTH-01 (Login): kein Tenant-Bezug
- F-AUTH-03 (User-Verwaltung): kein Tenant-Bezug — aber in Multi-Tenant muss ein User einem Tenant zugeordnet sein
- F-COMP-01 (Firma anlegen): kein `tenant_id` in Feld-Tabelle (Zeile 156-186)
- F-CONT-01 (Kontakt anlegen): kein `tenant_id` in Feld-Tabelle (Zeile 300-333)
- F-COMP-05 (Pagination): kein Tenant-Filter erwähnt
- F-COMP-06 (Suche): kein Tenant-Scoping erwähnt
**Fazit:** Multi-Tenant wurde später hinzugefügt und die frühen Requirements wurden nicht nachträglich aktualisiert. Das führt zu einer Lücke: Wie verhält sich F-COMP-01 (Firma anlegen) in Multi-Tenant-Kontext? Wird die Firma automatisch dem aktiven Tenant zugeordnet? Kann ein User Firmen in mehreren Tenants anlegen? Diese Fragen sind in den Requirements nicht beantwortet.
### 1.3 KI-Copilot (F-AI-01) mit voller API-Kontrolle vs. ältere UI-only-Flow-Requirements (WARNING)
**F-AI-01 (Zeile 798-806)** deklariert:
> „Der Copilot hat Zugriff auf die volle API und soll alles steuern können — Daten abfragen, erstellen, bearbeiten, löschen, Aktionen auslösen, Workflows triggern."
**F-CORE-06 (Zeile 899-902)** deklariert API-First:
> „Alle Core-Features und Plugin-Features sind primär über die API nutzbar. Die UI ist ein API-Client."
**Aber:** Mehrere Requirements beschreiben nur UI-Flows ohne API-Bezug:
- F-UI-01 (Responsive Design, Zeile 495-503): nur CSS-Breakpoints, kein API-Bezug
- F-UI-02 (i18n, Zeile 509-517): nur Frontend-Library, kein API-Bezug
- F-UI-03 (Toast-Notifications, Zeile 523-531): nur Frontend-Komponente
- F-UI-04 (Loading-States, Zeile 537-545): nur Frontend-State
- F-UI-05 (Empty-States, Zeile 551-559): nur Frontend-Komponente
- F-UI-06 (Confirmation-Dialogs, Zeile 565-573): nur Frontend-Modal
- F-UI-08 (Datenansichten, Zeile 579-582): nur Frontend-Toggle
**Einschränkung:** Diese UI-Requirements sind legitimerweise UI-only — sie beschreiben Präsentationslogik, keine Datenoperationen. F-CORE-06 sollte explizit ausschließen, dass reine UI-Präsentations-Features keine API-Entpunkte benötigen. Aktuell ist die Formulierung „alle Features über API nutzbar" zu breit und suggeriert, dass auch Toast-Notifications einen API-Endpunkt haben müssten.
**Zusätzlicher Befund:** F-AI-01 und F-CORE-06 wurden retroaktiv hinzugefügt. Die ursprünglichen Requirements (v0.1, archiviert in Appendix A, Zeile 2089-2128) beschreiben Jinja2-Templates und SQLite — eine völlig andere Architektur. Die Datei hat also mindestens drei Evolutionsschichten:
1. v0.1: Single-Tenant, Jinja2, SQLite (archiviert)
2. v0.3: React SPA, PostgreSQL, RBAC (Hauptteil)
3. v0.5+: Multi-Tenant, Plugin-System, API-First, KI-Copilot, Mail/Kalender/DMS (hinzugefügt)
Die Schichten wurden nicht vollständig integriert — Rückbezüge fehlen.
### 1.4 Auth-Mechanismus-Unschärfe (WARNING)
**F-AUTH-01 (Zeile 58):** „Session-basierte Auth mit HttpOnly+Secure+SameSite=Strict Cookie"
**F-AUTH-02 (Zeile 72-78):** Test-Szenario sagt „Token wird entfernt" und Akzeptanzkriterium sagt „Server-Token-Blacklist optional für v1" — das suggeriert Token-basierte Auth (JWT?), nicht Session-basierte Auth.
**F-INT-02 (Zeile 714-722):** „API-Endpunkte sind via Session-Cookie authentifiziert" aber erwähnt auch „Optional: API-Key für externe Integrationen".
**F-SEC-03 (Zeile 616-624):** „Session läuft nach 8h ab" — aber „Token gültig <8h" und „Token nach 8h → API gibt 401" — wieder Token-Sprache.
**Fazit:** Die Datei wechselt inkonsistent zwischen „Session" und „Token". Entweder es ist Session-basiert (Cookie + Server-Side Session Store) oder Token-basiert (JWT Stateless). Das muss entschieden und einheitlich formuliert werden.
---
## Section 2: Requirements vs. Bauanleitung Assessment
### 2.1 Enthaltene Implementierungsdetails
Die Datei enthält massiv Implementierungsdetails, die in eine Requirements-Spec nicht gehören:
#### HTTP-Endpunkte (Architektur, nicht Requirement)
Jedes einzelne Akzeptanzkriterium spezifiziert konkrete HTTP-Endpunkte mit Pfaden, HTTP-Methoden, Query-Parametern und Response-Codes:
- `POST /api/auth/login` (Zeile 65)
- `GET /api/companies/{id}` (Zeile 207)
- `DELETE /api/companies/{id}?cascade=true|false` (Zeile 235)
- `GET /api/contacts?page=1&page_size=25&sort_by=last_name&sort_order=asc` (Zeile 396)
- `POST /api/dms/files/upload` (Zeile 1013)
- `GET /api/dms/files/{id}/preview` (Zeile 1041)
- `POST /api/calendar/entries` (Zeile 1439)
- `GET /api/calendar/kanban?period=this_week` (Zeile 1419)
- `POST /api/mail/send` (Zeile 1693)
- `GET /api/mail/search?q=angebot&folder=inbox` (Zeile 1709)
- ...und dutzende weitere
**Problem:** Der Endpunkt-Pfad ist eine Architekturentscheidung. Ein Requirement sagt „User kann sich einloggen" — der Pfad `/api/auth/login` ist Implementierung.
#### DB-Schema-Definitionen (Architektur, nicht Requirement)
- **F-COMP-01 (Zeilen 156-186):** Vollständige Feld-Tabelle mit Typen: `String(100)`, `Integer`, `Decimal`, `Picklist`, `FK→Company`, `Text(32000)`, etc. — das ist ein DB-Schema
- **F-CONT-01 (Zeilen 300-333):** Vollständige Feld-Tabelle für Kontakte mit Typen
- **F-COMP-07 (Zeile 277):** `audit_log` Tabellenname
- **F-COMP-08 (Zeile 291):** `deletion_log` Tabellenname
- **F-CONT-07 (Zeile 424):** `company_contacts` N:M-Tabellenname
- **F-CORE-02 (Zeile 872):** `tenant_id` Feld auf allen Tabellen
- **F-MAIL-03 (Zeile 1709):** `tsvector`-Index, `mail_body_tsv`, `mail_subject_tsv`
- **F-CAL-12 (Zeile 1572):** `user_calendar_visibility` Tabellenname
- **F-CAL-15 (Zeile 1614):** `assigned_to: user_id` Feldname
**Problem:** Feldnamen, -typen und Tabellennamen sind Implementierungsdetails, die in das DB-Schema der Architektur gehören.
#### Technologie-Entscheidungen (Architektur, nicht Requirement)
- **F-CORE-07 (Zeile 907):** „Celery + Redis oder RQ + Redis" — Technologie-Wahl
- **F-CORE-08 (Zeile 914):** „Redis als Cache-Backend" — Technologie-Wahl
- **F-CORE-10 (Zeile 928):** „S3-kompatibles Storage (z.B. MinIO)" — Technologie-Wahl
- **F-MAIL-02 (Zeile 1693):** „DOMPurify" — Library-Wahl
- **F-MAIL-12 (Zeile 1846):** „python-gnupg" — Library-Wahl
- **F-UI-02 (Zeile 517):** „react-i18next" — Library-Wahl
- **F-DMS-04 (Zeile 1034):** „PDF.js" — Library-Wahl
- **F-DATA-03 (Zeile 459):** „Pydantic-Schemas" — Library-Wahl
- **F-INFRA-03 (Zeile 666):** „Python logging mit JSON-Formatter" — Library-Wahl
#### Protokoll-Details (Architektur, nicht Requirement)
- **F-MAIL-01 (Zeile 1670):** „IMAP4rev1 (RFC 3501)", „IMAP IDLE (RFC 2177)"
- **F-MAIL-02 (Zeile 1693):** „multipart/mixed", „SMTP-Versand"
- **F-MAIL-05 (Zeile 1733):** „References- und In-Reply-To-Header (RFC 5322)"
- **F-MAIL-18 (Zeile 1929):** „AES-256, Key via Env-Var"
- **F-CAL-08 (Zeile 1516):** „RRULE (RFC 5545)"
- **F-CAL-09 (Zeile 1530):** „RFC 5545 konform"
- **F-MAIL-18 (Zeile 1929):** „IMAP MOVE (RFC 6851)"
#### Frontend-Komponenten-Namen (Architektur, nicht Requirement)
- **F-CAL-01 (Zeile 1405):** `CalendarView` Komponente
- **F-CAL-02 (Zeile 1419):** `KanbanCalendar` Komponente
- **F-FILEUI-01 (Zeile 1321):** `FileBrowser`, `SidebarTree`, `MainView` Komponenten
- **F-FILEUI-02 (Zeile 1335):** `Breadcrumb` Komponente
- **F-FILEUI-03 (Zeile 1349):** `ContextMenu` Komponente
- **F-FILEUI-04 (Zeile 1363):** Multi-Select-State in `FileBrowser`
- **F-MAIL-05 (Zeile 1741):** `ThreadView` Komponente
#### Farbcodes und UI-Implementierung (Architektur, nicht Requirement)
- **F-CAL-06 (Zeile 1485):** `{appointment+normal: "#3B82F6", task+normal: "#F59E0B", *+follow_up: "#F97316", *+private: "#9CA3AF"}` — konkrete Hex-Codes
- **F-COMP-04 (Zeile 235):** `deleted_at = NOW` — SQL-Ausdruck
- **F-FILEUI-02 (Zeile 1335):** „Materialized Path oder rekursive Abfrage" — DB-Pattern
- **F-FILEUI-06 (Zeile 1391):** „HTML5 Drag & Drop API" — Browser-API
- **F-FILEUI-05 (Zeile 1377):** „XMLHttpRequest (für Progress-Events) oder WebSocket" — Technologie
#### Algorithmus- und Logik-Details (Architektur, nicht Requirement)
- **F-MAIL-07 (Zeilen 1762-1771):** Regelauswertungs-Reihenfolge, Background-Worker-Trigger
- **F-MAIL-08 (Zeile 1786):** `vacation_sent_log`, No-Reply-Erkennung: „noreply", „no-reply", „donotreply"
- **F-CAL-08 (Zeile 1516):** Recurrence-Instanz-Generierung, Exception-Handling
- **F-CAL-15 (Zeile 1614):** Notification-Versand bei Zuweisung
### 2.2 Schätzung des Anteils
| Kategorie | Zeilen (geschätzt) | Anteil |
|-----------|--------------------|--------|
| **Genuine Requirements (das WAS)** | ~700-750 | ~35% |
| — Projektbeschreibung, Domain Knowledge | ~25 | |
| — Feature-Anforderung-Texte („User kann...") | ~250 | |
| — Test-Szenarien (Verhalten, nicht Implementation) | ~300 | |
| — Non-funktionale Anforderungen | ~20 | |
| — Annahmen, Non-Goals, Checkliste, Open Questions | ~155 | |
| **Architektur/Implementierung (das HOW)** | ~1380-1430 | ~65% |
| — HTTP-Endpunkte in Akzeptanzkriterien | ~400 | |
| — DB-Schema-Definitionen (Feld-Tabellen, Typen) | ~150 | |
| — F-CORE-01 bis F-CORE-13 (Architekturentscheidungen) | ~100 | |
| — F-PLUGIN-01/02 (Plugin-System-Architektur) | ~20 | |
| — F-WF-01 (Workflow-Engine-Architektur) | ~10 | |
| — Protokoll-Details (RFCs, IMAP, SMTP) | ~80 | |
| — Technologie-/Library-Wahlen | ~60 | |
| — Frontend-Komponenten-Namen | ~40 | |
| — Farbcodes, SQL-Ausdrücke, Algorithmus-Details | ~50 | |
| — Redundanzen (F-FILE vs F-DMS, F-SCHED vs F-CORE-07) | ~100 | |
| — Historische/archivierte Requirements (Appendix A) | ~40 | |
| — Formatierung, Leerzeilen, Trennlinien | ~370 | |
**Fazit:** Die Datei ist zu ~35% eine Requirements-Spec und zu ~65% eine Architektur-/Implementierungs-Dokumentation. Sie hat den Charakter einer Bauanleitung angenommen, nicht den einer Anforderungsspezifikation.
---
## Section 3: Empfehlung
### 3.1 Was in requirements.md bleiben sollte
**Genuine Requirements — das WAS:**
1. **Projektbeschreibung** (Zeilen 10-14) — Was ist das Projekt?
2. **Domain Knowledge** (Zeilen 17-31) — Fachliche Begriffe und Referenzen
3. **Tech-Stack-Entscheidungen** (Zeilen 34-52) — Hohe-Level-Entscheidungen (Backend, DB, Frontend, Deployment)
4. **Feature-Anforderungstexte** — Die „Anforderung:"-Absätze jedes Features, bereinigt um Implementierungsdetails:
- F-AUTH-01 bis F-AUTH-08: Was muss die Auth können?
- F-COMP-01 bis F-COMP-08: Was muss Firmen-Management können?
- F-CONT-01 bis F-CONT-07: Was muss Kontakt-Management können?
- F-DATA-01 bis F-DATA-06: Was muss Daten-Management können?
- F-UI-01 bis F-UI-08: Was muss die UI bieten?
- F-SEC-01 bis F-SEC-03: Welche Sicherheitsanforderungen?
- F-INFRA-01 bis F-INFRA-04: Welche Infrastrukturanforderungen?
- F-MIG-01: Was muss Migration/Import können?
- F-INT-01: Welche Integrationsanforderung?
- F-TEST-01: Welche Test-Strategie?
- F-ENV-01: Welche Environment-Anforderung?
- F-DOC-01: Welche Doku-Anforderung?
- F-PERF-01: Welche Performance-Anforderung?
- F-SEARCH-01: Was muss die globale Suche können?
- F-NAV-01: Welche Navigation?
- F-SET-01: Welche Einstellungen?
- F-DMS-01 bis F-DMS-07: Was muss DMS können? (ohne Endpunkte)
- F-LINK-01 bis F-LINK-06: Was muss Verknüpfung können? (ohne Endpunkte)
- F-TAG-01 bis F-TAG-04: Was muss Tagging können? (ohne Endpunkte)
- F-PERM-01 bis F-PERM-06: Welche Berechtigungs-Requirements? (ohne Endpunkte)
- F-FILEUI-01 bis F-FILEUI-06: Welche UI-Requirements für Datei-Browser? (ohne Komponentennamen)
- F-CAL-01 bis F-CAL-18: Was muss Kalender können? (ohne Endpunkte, ohne Farbcodes)
- F-MAIL-01 bis F-MAIL-19: Was muss Mail können? (ohne Protokoll-Details)
- F-AI-01: Was muss der KI-Copilot können?
- F-SCHED-01: Welche Background-Job-Anforderung?
5. **Test-Szenarien** — Aber bereinigt: nur Verhalten beschreiben („User klickt X → Y passiert"), keine Implementierung („`deleted_at = NOW` gesetzt", „`tsvector`-Index")
6. **Non-funktionale Anforderungen** (Zeilen 1957-1973) — Bleiben, aber Metriken ohne Library-Namen
7. **Annahmen** (Zeilen 1976-1999) — Bleiben
8. **Non-Goals** (Zeilen 2001-2046) — Bleiben
9. **Discovery-Checkliste** (Zeilen 2049-2073) — Bleibt
10. **Open Questions** (Zeilen 2077-2085) — Bleibt
### 3.2 Was nach architecture.md verschoben werden sollte
**Architektur/Implementierung — das HOW:**
1. **F-CORE-01 bis F-CORE-13 (Zeilen 864-953):** Komplett in architecture.md
- Event Bus, Tenant-Isolation (`tenant_id`), Plugin-Migration, UI-Plugin-Framework, Service Container/DI, API-First (Endpunkt-Versionierung `/api/v1/`), Async Job Queue (Celery/Redis), Caching (Redis), Storage-Backend (S3/MinIO), Import/Export Service, PDF-Gen, Notification Service
2. **F-PLUGIN-01, F-PLUGIN-02 (Zeilen 848-860):** Plugin-System-Architektur → architecture.md
- Plugin-Schnittstelle, Manifest-Format, Lifecycle-Hooks, Abhängigkeiten
3. **F-WF-01 (Zeile 812-815):** Workflow-Engine-Architektur → architecture.md
- Hybrid-Ansatz, Code-Engine vs. konfigurierbare Regeln
4. **Alle HTTP-Endpunkt-Spezifikationen:** → architecture.md (API-Contract-Sektion)
- `POST /api/auth/login`, `GET /api/companies/{id}`, etc.
- Request/Response-Body-Formate
- Query-Parameter-Spezifikationen
- HTTP-Status-Codes
5. **Alle DB-Schema-Definitionen:** → architecture.md (DB-Schema-Sektion)
- Feld-Tabellen mit Typen (F-COMP-01 Zeilen 156-186, F-CONT-01 Zeilen 300-333)
- Tabellennamen (`audit_log`, `deletion_log`, `company_contacts`, `user_calendar_visibility`)
- `tenant_id`-Feld-Spezifikation
- `tsvector`-Index-Spezifikation
6. **Protokoll-Details:** → architecture.md
- IMAP4rev1, IMAP IDLE, IMAP MOVE, SMTP-Auth
- RFC 5545 (RRULE), RFC 5322 (Threading)
- PGP-Verschlüsselung (python-gnupg)
- DOMPurify-Sanitization
- AES-256-Verschlüsselung für Passwörter
7. **Frontend-Komponenten-Architektur:** → architecture.md (Frontend-Architektur-Sektion)
- Komponenten-Namen (`CalendarView`, `KanbanCalendar`, `FileBrowser`, `Breadcrumb`, `ContextMenu`, `ThreadView`)
- State-Management (`Multi-Select-State`, `user_calendar_visibility`)
- HTML5 Drag & Drop API, XMLHttpRequest
- Materialized Path Pattern
8. **Farbcodes und UI-Mappings:** → architecture.md oder design-system.md
- Hex-Codes für Kalender-Typen
- Farb-Mapping-Logik
9. **Algorithmus-Details:** → architecture.md
- Mail-Regel-Auswertung
- Auto-Reply-Logik (No-Reply-Erkennung, `vacation_sent_log`)
- Recurrence-Instanz-Generierung
- Thread-Gruppierung
10. **F-FILE-01 bis F-FILE-04 (Zeilen 955-985):** Duplikate von F-DMS/F-PERM — entfernen oder konsolidieren
11. **F-SCHED-01 (Zeile 784-792):** Duplikat von F-CORE-07 — konsolidieren
12. **Appendix A: Historische Anforderungen (Zeilen 2089-2128):** In separates `changelog.md` oder entfernen
### 3.3 Wie die Widersprüche (Plugin vs. Core-Feature) aufgelöst werden können
**Option A: Module sind Core-Features (empfohlen für v1/v2)**
- Entferne F-PLUGIN-01, F-PLUGIN-02, F-CORE-01 bis F-CORE-13 aus requirements.md
- Module (Mail, Kalender, DMS, Tags) sind Core-Features mit Requirements
- Plugin-System ist ein Non-Goal für v1/v2 („Plugin-System für spätere Versionen")
- Vorteil: Konsistent, weniger Komplexität, schneller implementierbar
- Nachteil: Weniger Erweiterbarkeit
**Option B: Module sind Plugins**
- Core-Requirements definieren nur Plugin-Schnittstelle und Core-Infrastruktur
- Plugin-Requirements (Mail, Kalender, DMS) werden in separate Plugin-Specs ausgelagert
- Core-Requirements sagen: „Das System unterstützt Plugins. Plugin 'Mail' muss X können. Plugin 'Kalender' muss Y können."
- Die detaillierten Feature-Spezifikationen (F-MAIL-*, F-CAL-*, F-DMS-*) wandern in Plugin-Requirements
- Vorteil: Saubere Trennung, Erweiterbarkeit
- Nachteil: Mehr Dokumentation, mehr Komplexität, Over-Engineering für ein Mini-CRM
**Empfehlung: Option A für v1/v2.**
Ein Mini-CRM mit 10 concurrent Users braucht kein Plugin-System. Das Plugin-System ist ein Architektur-Non-Goal für v1/v2. Die Module werden als Core-Features implementiert. Wenn Erweiterbarkeit später benötigt wird, kann ein Plugin-System in v3+ hinzugefügt werden. F-PLUGIN-01, F-PLUGIN-02, F-CORE-01 bis F-CORE-13 werden zu Non-Goals.
---
## Section 4: Spezifische Konflikte (Tabelle)
| ID/Zeile | Issue | Severity | Vorschlag |
|----------|-------|----------|-----------|
| F-PLUGIN-01 (848) vs F-DMS/F-CAL/F-MAIL | Module als Plugins deklariert, aber als Core-Features mit Endpunkten/DB-Schemas spezifiziert | **critical** | Plugin-System als Non-Goal für v1/v2; Module als Core-Features deklarieren |
| F-FILE-01-04 (955-985) vs F-DMS-01-07 (991-1083) | F-FILE und F-DMS beschreiben dasselbe Modul mit unterschiedlichen IDs. F-FILE-01 (Datei-Explorer) = F-DMS-01 (Ordner-Struktur), F-FILE-03 (PDF-Preview) = F-DMS-04, F-FILE-04 (OnlyOffice) = F-DMS-05 | **critical** | F-FILE-01 bis F-FILE-04 entfernen; durch F-DMS-Referenzen ersetzen |
| F-FILE-03 (973) vs F-DMS-04 (1033) | Beide spezifizieren PDF-Preview im Browser — Duplikat | **critical** | F-FILE-03 entfernen; F-DMS-04 behalten (detaillierter) |
| F-FILE-04 (982) vs F-DMS-05 (1047) | Beide spezifizieren OnlyOffice-Integration — Duplikat | **critical** | F-FILE-04 entfernen; F-DMS-05 behalten (detaillierter) |
| F-FILE-02 (964) vs F-PERM-03/04 (1257-1279) | F-FILE-02 (Datei-Sharing) ist vereinfachte Version von F-PERM-03/04 — Redundanz | **warning** | F-FILE-02 entfernen; F-PERM-03/04 als maßgeblich deklarieren |
| F-SCHED-01 (784) vs F-CORE-07 (906) | Beide beschreiben Background-Jobs/Async-Queue — F-SCHED-01 ist vereinfachte Version von F-CORE-07 | **warning** | F-SCHED-01 entfernen; F-CORE-07 in architecture.md verschieben; Requirement „lange Operationen als Background-Job" in requirements.md behalten |
| F-DATA-01/02 (430-452) vs F-CORE-11 (934) | CSV/Excel-Export (F-DATA) überlappt mit Generic Import/Export Service (F-CORE-11) | **warning** | F-CORE-11 in architecture.md; F-DATA-01/02 in requirements.md behalten (das WAS); F-CORE-11 beschreibt das HOW |
| F-AUTH-07 (135) vs F-AUTH-01-F-CONT-07 (57-410) | Multi-Tenant deklariert, aber frühe Requirements erwähnen Tenant-Kontext nicht | **warning** | Frühe Requirements um Tenant-Bezug ergänzen: „Firma wird dem aktiven Tenant zugeordnet", „Suche ist Tenant-gefiltert" |
| F-AUTH-01 (58) vs F-AUTH-02 (72-78) | F-AUTH-01: „Session-basiert", F-AUTH-02: „Token wird entfernt", „Server-Token-Blacklist" — inkonsistente Terminologie | **warning** | Einheitlich „Session" verwenden; Token-Blacklist entfernen oder klar als Session-Invalidierung benennen |
| F-SEC-03 (616) vs F-AUTH-01 (58) | F-SEC-03 spricht von „Token" („Token gültig <8h", „Token nach 8h → 401"), F-AUTH-01 von „Session-Cookie" | **warning** | Einheitlich Session-basiert formulieren; „Session läuft nach 8h ab" |
| F-CORE-06 (899) vs F-UI-01-06 (495-573) | API-First („alle Features über API") vs. reinen UI-Features ohne API-Bezug (Toast, Loading-States, Empty-States) | **warning** | F-CORE-06 einschränken: „Alle Daten- und Funktions-Features über API nutzbar; reine UI-Präsentations-Features (Loading-States, Toasts) ausgenommen" |
| F-AUTH-06 (126) vs F-AUTH-04 (98) | F-AUTH-06 (Multi-User mit Rollen) überlappt mit F-AUTH-04 (RBAC) — F-AUTH-06 ist detailliertere Version | **warning** | Zusammenführen oder F-AUTH-06 als Erweiterung von F-AUTH-04 kennzeichnen |
| F-AUTH-08 (144) vs F-AUTH-04/06 (98-129) | F-AUTH-08 (Feld-Ebene-Granularität) erweitert F-AUTH-04/06, wird aber nicht kreuzreferenziert | **warning** | F-AUTH-08 als Unterpunkt von F-AUTH-04/06 integrieren oder explizit referenzieren |
| F-SEARCH-01 (821) vs F-COMP-06 (255)/F-CONT-06 (402) | Globale Suche überlappt mit Firmen-/Kontakt-Suche — keine klare Abgrenzung | **warning** | F-SEARCH-01 als übergeordnete Suche deklarieren; F-COMP-06/F-CONT-06 als Modul-Suche mit Querverweis |
| F-INT-01 (700) vs F-MAIL-02 (1683) | E-Mail-Integration für Passwort-Reset (F-INT-01) ist Subset des vollen Mail-Moduls (F-MAIL-02) | **info** | F-INT-01 als v1-Requirement behalten; F-MAIL-02 als v2-Erweiterung kennzeichnen; F-INT-01 bei F-MAIL-02 referenzieren |
| F-CAL-10 (1536) vs Non-Goals (2028) | F-CAL-10 (Ressourcen-Booking) als „Optional für später (post-v2)" markiert, hat aber volle Test-Szenarien und Akzeptanzkriterien | **warning** | Entweder zu Non-Goals verschieben oder als v2-Feature belassen mit klarer Markierung „post-v2" |
| F-COMP-01 Feldtabelle (156-186) | DB-Schema mit Typen (String(100), Integer, Decimal) in Requirements | **info** | Feldliste als „Felder, die erfasst werden" in requirements.md; Typen und Constraints in architecture.md |
| F-CONT-01 Feldtabelle (300-333) | DB-Schema mit Typen in Requirements | **info** | Analog zu F-COMP-01 |
| F-COMP-04 (235) | `deleted_at = NOW` (SQL-Ausdruck) in Akzeptanzkriterium | **info** | „Firma wird als gelöscht markiert (Soft-Delete)" — ohne SQL |
| F-CONT-07 (424) | `company_contacts` Tabellenname in Akzeptanzkriterium | **info** | „N:M-Verknüpfung wird erstellt" — ohne Tabellennamen |
| F-CAL-06 (1485) | Hex-Farbcodes in Akzeptanzkriterium | **info** | „Farbe wird basierend auf Typ zugeordnet" — Farbwerte in design-system.md |
| F-CAL-08 (1516) | RRULE (RFC 5545) in Akzeptanzkriterium | **info** | „Wiederholungsmuster werden unterstützt" — RFC-Referenz in architecture.md |
| F-MAIL-03 (1709) | `tsvector`-Index in Akzeptanzkriterium | **info** | „Volltext-Suche über alle Mails" — Index-Strategie in architecture.md |
| F-MAIL-01 (1677) | „IMAP IDLE-Listener läuft als Background-Task" in Akzeptanzkriterium | **info** | „Neue Mails werden innerhalb von 5 Sekunden angezeigt" — Implementierung in architecture.md |
| F-MAIL-02 (1693) | „DOMPurify" in Akzeptanzkriterium | **info** | „HTML wird sanitisiert" — Library in architecture.md |
| F-MAIL-12 (1846) | „python-gnupg" in Akzeptanzkriterium | **info** | „PGP-Verschlüsselung wird unterstützt" — Library in architecture.md |
| F-FILEUI-01 (1321) | `FileBrowser`, `SidebarTree`, `MainView` Komponentennamen | **info** | „Datei-Browser mit Baum-Ansicht und Hauptbereich" — Komponentennamen in architecture.md |
| F-FILEUI-02 (1335) | „Materialized Path oder rekursive Abfrage" in Akzeptanzkriterium | **info** | „Pfad wird aus Ordner-Hierarchie generiert" — Pattern in architecture.md |
| F-FILEUI-06 (1391) | „HTML5 Drag & Drop API" in Akzeptanzkriterium | **info** | „Drag & Drop wird unterstützt" — API in architecture.md |
| F-FILEUI-05 (1377) | „XMLHttpRequest oder WebSocket" in Akzeptanzkriterium | **info** | „Upload-Progress wird angezeigt" — Technologie in architecture.md |
| F-CORE-07 (907) | „Celery + Redis oder RQ + Redis" — Technologie-Wahl in Requirements | **info** | Komplett in architecture.md |
| F-CORE-08 (914) | „Redis als Cache-Backend" — Technologie-Wahl in Requirements | **info** | Komplett in architecture.md |
| F-CORE-10 (928) | „S3-kompatibles Storage (z.B. MinIO)" — Technologie-Wahl in Requirements | **info** | Komplett in architecture.md |
| F-CORE-02 (872) | `tenant_id`-Feld-Spezifikation in Requirements | **info** | „Daten sind pro Tenant isoliert" — `tenant_id` in architecture.md |
| DISCOVERY_CHECK (2131) | Behauptet `features_with_ids=127/127` — tatsächlich sind es ~141 aktive Feature-IDs | **warning** | Zählung korrigieren oder klären, welche Features gezählt wurden |
| F-DATA-05 fehlt | Springt von F-DATA-04 (Zeile 472) zu F-DATA-06 (Zeile 481) — F-DATA-05 existiert nicht | **info** | Entweder F-DATA-05 nachtragen oder Nummerierung korrigieren |
| F-UI-07 fehlt | Springt von F-UI-06 (Zeile 565) zu F-UI-08 (Zeile 579) — F-UI-07 existiert nicht | **info** | Entweder F-UI-07 nachtragen oder Nummerierung korrigieren |
| F-COMP-07 (269) vs F-COMP-08 (283) | Audit-Log und DSGVO-Löschung haben überlappende Belange (beide behandeln Logging von Löschungen), Interaktion nicht dokumentiert | **info** | Klarstellen: Audit-Log = schreibende Aktionen; DSGVO-Löschung = harte Löschung inkl. Audit-Log-Einträgen, separate `deletion_log` |
| NF-06 (1966) | Code-Struktur (`api/`, `models/`, `schemas/`, `services/`, `tests/`) in nicht-funktionaler Anforderung | **info** | In architecture.md verschieben; in requirements.md: „Code-Struktur ist klar getrennt" |
| Appendix A (2089-2128) | Historische v0.1-Requirements mit veralteten Tech-Stack (Jinja2, SQLite, Python 3.11) | **info** | In `changelog.md` verschieben oder entfernen; verwirrend in requirements.md |
---
## Zusammenfassung
| Metrik | Wert |
|--------|------|
| Gesamtzeilen | 2131 |
| Aktive Feature-IDs | ~141 |
| Genuine Requirements-Anteil | ~35% |
| Architektur/Implementierungs-Anteil | ~65% |
| Critical Issues | 4 |
| Warning Issues | 14 |
| Info Issues | 21 |
| Empfehlung | Requirements bereinigen, ~65% nach architecture.md verschieben, Plugin-System als Non-Goal für v1/v2 |
**Urteil:** Die Datei ist eine Mischung aus Requirements-Spec und Architektur-Dokument. Sie hat den Charakter einer Bauanleitung angenommen. Für eine saubere Trennung sollten ~65% des Inhalts in architecture.md verschoben werden. Die verbleibende requirements.md sollte nur das WAS beschreiben — nicht das HOW.
-2142
View File
File diff suppressed because it is too large Load Diff