Files
leocrm/docs/phase0_phase1_acceptance_report.md
T
Agent Zero a760a759eb Phase 0+3: Stand sichern, alte Doku einfrieren, doppelte Command-Struktur entfernen
Phase 0:
- Git Tag: pre-recovery-current (3cbf921)
- Branch: recovery/minimal-finish
- docs/RECOVERY_SCOPE.md als verbindliche Quelle
- Alte Dokumente als UEBERHOLT markiert

Phase 3:
- app/core/commands.py entfernt (ungenutzte Doppelstruktur)
- app/commands/create_contact.py entfernt (ungenutzte Doppelstruktur)
- 24/24 Command-Tests bestanden — produktive Commands unbeeinflusst
2026-08-03 13:25:48 +02:00

16 KiB
Raw Blame History

Ü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: stvabl4vaqru7jclx4ittzr3:3032ad2 (Coolify-Build aus Git)


Container-Status

Container Status Rolle
stvabl4vaqru7jclx4ittzr3-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 --silentnpm 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

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_tlsuse_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: stvabl4vaqru7jclx4ittzr3: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 stvabl4vaqru7jclx4ittzr3: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: stvabl4vaqru7jclx4ittzr3: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 stvabl4vaqru7jclx4ittzr3: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.