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:
@@ -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 (0001–0090) │
|
||||
│ ├── 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:** 8–16 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:** 6–10 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:** 12–24 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:** 14–24 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:** 30–50 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:** 12–20 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:** 10–18 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:** 16–28 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:** 12–20 Stunden
|
||||
|
||||
---
|
||||
|
||||
## 3. Gesamtschätzung
|
||||
|
||||
### Reine Codeänderungen
|
||||
|
||||
| Phase | Beschreibung | Aufwand |
|
||||
|------|-------------|---------|
|
||||
| 0+1 | Ausgangsbasis, Login, DB-Rollen, RLS | ✅ Abgeschlossen |
|
||||
| 2 | Datenintegrität | 8–16 h |
|
||||
| 3 | Plugin-Lifecycle | 6–10 h |
|
||||
| 4 | Sichere KI-Delegation | 12–24 h |
|
||||
| 5 | Transactional Outbox | 14–24 h |
|
||||
| 6 | Workspaces | 30–50 h |
|
||||
| 7 | DMS und Attachments | 12–20 h |
|
||||
| 8 | Sicherheitsreste und Build | 10–18 h |
|
||||
| 9 | CI und Quality Gates | 16–28 h |
|
||||
| 10 | Backup, Restore, Monitoring | 12–20 h |
|
||||
| **Gesamt** | **Verbleibend** | **120–210 h** |
|
||||
|
||||
### Einschließlich Migrationen, Tests und Deployment
|
||||
|
||||
| Bereich | Aufwand |
|
||||
|----------|---------|
|
||||
| Verbleibende Codeänderungen | 120–210 h |
|
||||
| Tests, Fehlerkorrekturen, Deployment | +30–50 h |
|
||||
| **Gesamt verbleibend** | **150–260 h** |
|
||||
|
||||
### Pilotfähiger technischer Kern (ohne vollständige Workspaces)
|
||||
|
||||
| Bereich | Aufwand |
|
||||
|----------|---------|
|
||||
| Datenintegrität | 8–16 h |
|
||||
| Plugin-Lifecycle | 6–10 h |
|
||||
| Sichere KI-Delegation | 12–24 h |
|
||||
| Outbox | 14–24 h |
|
||||
| DMS und Attachments | 12–20 h |
|
||||
| Sicherheitsreste und Build | 10–18 h |
|
||||
| CI | 16–28 h |
|
||||
| Backup, Restore, Monitoring | 12–20 h |
|
||||
| **Gesamt (ohne Workspaces)** | **90–160 h** |
|
||||
|
||||
### Vollständige Workspaces zusätzlich
|
||||
|
||||
| Bereich | Aufwand |
|
||||
|----------|---------|
|
||||
| Workspaces | 30–50 h |
|
||||
| **Gesamt einschließlich Workspaces** | **120–210 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.*
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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 |
|
||||
|
||||
@@ -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) |
|
||||
@@ -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 |
|
||||
@@ -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.
|
||||
@@ -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-01–04: alle `[v2-Plugin]` (vorher `[v1-Plugin]`)
|
||||
- F-DMS-01–07: alle `[v2-Plugin]`
|
||||
- F-LINK-01–06: alle `[v2-Plugin]`
|
||||
- F-TAG-01–04: alle `[v2-Plugin]`
|
||||
- F-PERM-01–06: alle `[v2-Plugin]`
|
||||
- F-FILEUI-01–06: alle `[v2-Plugin]`
|
||||
- F-CAL-01–18: alle `[v2-Plugin]` (F-CAL-10 = `[v2-Plugin — später]`)
|
||||
- F-MAIL-01–19: 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-01–03: 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 1940–1968)
|
||||
- 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 1918–1936)
|
||||
- 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-01–F-AUTH-08 | 8 (01-08) | ✅ |
|
||||
| Companies | F-COMP-01–F-COMP-08 | 8 (01-08) | ✅ |
|
||||
| Contacts | F-CONT-01–F-CONT-07 | 7 (01-07) | ✅ |
|
||||
| Data | F-DATA-01–F-DATA-04, F-DATA-06 | 5 (01-04, 06) | ✅ (gap: kein F-DATA-05) |
|
||||
| UI | F-UI-01–F-UI-06, F-UI-08 | 7 (01-06, 08) | ✅ (gap: kein F-UI-07) |
|
||||
| Accessibility | F-A11Y-01–F-A11Y-03 | 3 (01-03) | ✅ |
|
||||
| Security | F-SEC-01–F-SEC-03 | 3 (01-03) | ✅ |
|
||||
| Infrastruktur | F-INFRA-01–F-INFRA-04 | 4 (01-04) | ✅ |
|
||||
| Migration | F-MIG-01 | 1 (01) | ✅ |
|
||||
| Integration | F-INT-01–F-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-01–F-CORE-13 | 13 (01-13) | ✅ |
|
||||
| Plugin-System | F-PLUGIN-01–F-PLUGIN-02 | 2 (01-02) | ✅ |
|
||||
| File | F-FILE-01–F-FILE-04 | 4 (01-04) | ✅ |
|
||||
| DMS | F-DMS-01–F-DMS-07 | 7 (01-07) | ✅ |
|
||||
| Links | F-LINK-01–F-LINK-06 | 6 (01-06) | ✅ |
|
||||
| Tags | F-TAG-01–F-TAG-04 | 4 (01-04) | ✅ |
|
||||
| Permissions | F-PERM-01–F-PERM-06 | 6 (01-06) | ✅ |
|
||||
| File-UI | F-FILEUI-01–F-FILEUI-06 | 6 (01-06) | ✅ |
|
||||
| Kalender | F-CAL-01–F-CAL-18 | 18 (01-18) | ✅ |
|
||||
| Mail | F-MAIL-01–F-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.*
|
||||
@@ -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.
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user