Files
crm-system/docs/03a-patterns-summary.md
T

27 KiB
Raw Blame History

CRM-Build Pattern-Summary aus wochenplaner-Repo

Phase 3 Codebase Exploration & Pattern Reuse Sub-Agent: codebase_explorer (READ-ONLY) Erstellt: 2026-06-03 22:24 UTC Quellen: https://forgejo.media-on.de/Leopoldadmin/wochenplaner (Branch master) Status: Wochenplaner-Patterns analysiert; rentman-clone NICHT verfügbar


Section A: Wochenplaner-Patterns (PRIMAER)

Wochenplaner-Architektur ist Multi-Container (backend + frontend mit nginx) im Gegensatz zur ursprünglichen Annahme "Single-Container mit content: mount". Tatsächliche Stack-Bestätigung:

Layer Stack
Webserver nginx (Reverse-Proxy + Static)
Backend FastAPI 0.111.0 + uvicorn 0.29.0, sync (nicht async!)
DB SQLite via SQLAlchemy 2.0.35 (sync!)
Auth python-jose 3.3.0 + passlib[bcrypt] 1.7.4 + bcrypt 4.0.1
Validation pydantic 2.7.0
HTTP-Fileupload python-multipart 0.0.9

Wichtige Abweichung zur CRM-Architektur (02-architecture.md):

  • Wochenplaner nutzt sync SQLAlchemy, CRM braucht async (aiosqlite/asyncpg)
  • Wochenplaner hat kein Alembic (nutzt Base.metadata.create_all), CRM braucht Alembic
  • Wochenplaner hat kein pytest/Coverage-Setup, CRM braucht es (NFR-4: ≥70%)
  • Wochenplaner hat kein Service-Layer-Pattern (Monolith in main.py, 12.9 KB), CRM braucht Service-Layer

Pattern A-1: FastAPI App-Init + CORS + Static-Mount

Was: Single-File-Setup mit CORS-Middleware, Static-Files-Mount, Mount-Check via os.path.isdir. Wo: backend/main.py Zeilen 13-30

import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles

app = FastAPI(title="Wochenplaner API", version="1.0.0")

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

FRONTEND_DIR = os.path.join(os.path.dirname(__file__), "frontend")
if os.path.isdir(FRONTEND_DIR):
    app.mount("/", StaticFiles(directory=FRONTEND_DIR, html=True), name="frontend")

CRM-Anwendung:

  • allow_origins=["*"] MUSS ersetzt werden durch Whitelist (NFR-2: CORS-Whitelist, kein *)
  • Static-Mount in app/main.py mounten unter /static (nicht / CRM hat eigene Routes)
  • os.path.isdir-Check übernehmen für optionalen Mount bei Dev-Skips

Pattern A-2: JWT-Setup + Token-Encode/Decode

Was: python-jose JWT mit HS256, 7-Tage-Expiry, Secret aus ENV mit Fallback. Wo: backend/main.py Zeilen 32-70

import os
from datetime import datetime, timedelta
from jose import JWTError, jwt

SECRET_KEY = os.environ.get("JWT_SECRET", "wochenplaner-jwt-secret-2024")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7  # 7 Tage

def create_access_token(data: dict) -> str:
    to_encode = data.copy()
    expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({"exp": expire})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

def verify_token(token: str) -> dict:
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        return payload
    except JWTError:
        raise HTTPException(status_code=401, detail="Ungültiger Token")

CRM-Anwendung:

  • JWT-Lib beibehalten (python-jose[cryptography] 3.3.0) für 1:1-Übernahme
  • Expiry anpassen: 02-architecture/01-requirements spezifiziert 24h für CRM (FR-1.2)
  • SECRET_KEY MUSS mindestens 32 Zeichen haben (NFR-2: ≥32 Zeichen). Fallback in Prod ist VERBOTEN.
  • verify_token erweitern um expired-vs-invalid Unterscheidung für token_expired Hint (FR-1.7)

Pattern A-3: Passwort-Hashing mit passlib + bcrypt

Was: CryptContext mit bcrypt-Schema, deprecated="auto" (rüstet Hashes automatisch up). Wo: backend/main.py Zeile 35

from passlib.context import CryptContext

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# Hashing
password_hash = pwd_context.hash("admin123")

# Verify
if not user or not pwd_context.verify(req.password, user.password_hash):
    raise HTTPException(status_code=401, detail="Ungültige Zugangsdaten")

CRM-Anwendung:

  • 1:1 übernehmen, passlib[bcrypt]==1.7.4 und bcrypt==4.0.1 exakt pinnen (Inkompatibilität 5.x)
  • NFR-2 fordert argon2id (Primary) oder bcrypt (Fallback) bcrypt ist OK
  • Default-Rounds sind 12 (passlib-Default), für CRM reicht das, ggf. auf 14 erhöhen wenn Hardware es erlaubt

Pattern A-4: get_current_user + require_role Factory

Was: Dual-Source-Auth (Header Bearer + Query-Param ?token=), Role-Factory-Pattern. Wo: backend/main.py Zeilen 60-100

from fastapi import Depends, Request, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from typing import Optional

security = HTTPBearer(auto_error=False)

def get_current_user(
    request: Request,
    credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)
) -> dict:
    token = None
    if credentials:
        token = credentials.credentials
    if not token:
        token = request.query_params.get("token")  # WebSocket-Fallback
    if not token:
        raise HTTPException(status_code=401, detail="Nicht authentifiziert")
    return verify_token(token)

def require_role(role: str):
    def checker(payload: dict = Depends(get_current_user)):
        user_role = payload.get("role", "user")
        if user_role == "admin":
            return payload  # Admin bypass
        if user_role != role:
            raise HTTPException(status_code=403, detail=f"Rolle '{role}' erforderlich")
        return payload
    return checker

# Nutzung:
@app.get("/api/admin/users")
def list_users(payload: dict = Depends(require_role("admin"))):
    ...

CRM-Anwendung:

  • get_current_user zurückgibt nicht dict, sondern ein User-Model (async DB-Load). Wochenplaner nutzt nur JWT-Payload (kein DB-Reload) CRM sollte User neu laden, damit is_active/Soft-Delete greift.
  • require_role für 3 CRM-Rollen: admin, manager, rep (statt admin/user/viewer)
  • Query-Param-Token-Fallback für WebSocket-Support ggf. beibehalten, aber NFR-2: Auth ausschließlich via Bearer-Header ist sicherer
  • auto_error=False beibehalten, damit 401 mit Custom-Detail möglich ist

Pattern A-5: get_db() Dependency (sync)

Was: Generator-Dependency mit try/finally, SessionLocal-Instanz. Wo: backend/main.py Zeilen 102-108

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

CRM-Anwendung:

  • Komplett NEU schreiben als AsyncSession-Variante:
# app/core/db.py (CRM)
async def get_db() -> AsyncGenerator[AsyncSession, None]:
    async with AsyncSessionLocal() as session:
        try:
            yield session
        except Exception:
            await session.rollback()
            raise
  • Wochenplaner-Sync-Pattern NICHT übernehmen, weil 02-architecture.md explizit async vorschreibt

Pattern A-6: SQLAlchemy-Engine + Session-Factory

Was: Engine mit check_same_thread=False (SQLite-Workaround), SessionLocal-Standardkonfig. Wo: backend/models.py Zeilen 10-15

from sqlalchemy import create_engine, Column, String, Text, Integer, Boolean, DateTime
from sqlalchemy.orm import declarative_base, sessionmaker, Session
import os

DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
os.makedirs(DATA_DIR, exist_ok=True)
DATABASE_URL = f"sqlite:///{DATA_DIR}/wochenplaner.db"

engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

CRM-Anwendung:

  • URL-Generierung adaptieren: if os.environ.get("DATABASE_URL"): postgres_url else: sqlite+aiosqlite:///./dev.db
  • check_same_thread entfällt komplett (async-Engine nutzt asyncpg/aiosqlite, kein Multi-Thread-Issue)
  • Base = declarative_base() ersetzen durch Base = DeclarativeBase + Type-Annotations (SQLAlchemy 2.0 Style)
  • Models: Spalten-Definitionen ohne Mapped[] (sync) → CRM mit Mapped[...] = mapped_column(...) (async 2.0)

Pattern A-7: Docker-Multi-Stage-Setup

Was: python:3.12-slim, requirements-copy-then-install (Cache), mkdir data-dir, uvicorn-CMD. Wo: backend/Dockerfile.backend (244 Bytes)

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN mkdir -p /app/data && chmod 777 /app/data
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

CRM-Anwendung:

  • 1:1 übernehmen, aber:
    • main:app ersetzen durch app.main:app (CRM nutzt app/-Package)
    • /app/data durch persistentes Volume oder PostgreSQL-Service ersetzen
    • Multi-Stage-Build empfohlen für CRM: Builder-Stage mit gcc, Final-Stage nur Runtime-Deps (kleinere Images)
    • Non-Root-User USER appuser für Production-Sicherheit
    • Alembic-Startup-Hook: CMD ["sh", "-c", "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000"]

Pattern A-8: docker-compose mit Coolify-Env-Pattern

Was: Multi-Service (backend + frontend mit nginx), Volume-Mount für SQLite, SERVICE_BASE64_64-Pattern für JWT-Secret. Wo: docker-compose.yml (429 Bytes) im Repo-Root

services:
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile.backend
    restart: unless-stopped
    environment:
      - PORT=80
      - JWT_SECRET=${SERVICE_BASE64_64_JWT:-wochenplaner-jwt-secret}
    volumes:
      - wochenplaner_data:/app/data

  frontend:
    build:
      context: .
      dockerfile: frontend/Dockerfile
    restart: unless-stopped

volumes:
  wochenplaner_data:
    driver: local

CRM-Anwendung:

  • Pattern 1:1 übernehmen, aber:
    • Service-Name crm-api statt backend
    • Service crm-web (nginx mit statischem Frontend) zusätzlich
    • 3. Service crm-db (PostgreSQL) hinzufügen gemäß 02-architecture §1 (Production: PostgreSQL statt SQLite)
    • ENV-Keys: JWT_SECRET, DATABASE_URL, AUTH_SECRET (≥32 Zeichen), CORS_ORIGINS
    • SERVICE_BASE64_64_*-Pattern ist Coolify-Standard für Auto-generierte Secrets (64 Zeichen base64) für CRM:
      • JWT_SECRET=${SERVICE_BASE64_64_JWT}
      • DATABASE_PASSWORD=${SERVICE_BASE64_32_DB}
  • Volume: Für SQLite-Dev crm_data:/app/data, für Production kein lokales Volume (PostgreSQL ist externer Service)
  • Domain-Eintrag: https://crm.media-on.de:80 (Port MUSS in URL Memory-regel)

Pattern A-9: nginx-Reverse-Proxy + SPA-Fallback

Was: nginx-Config mit /api/ Proxy zum Backend, /health direkter Proxy, alle anderen Routen → index.html (SPA-Fallback). Wo: nginx.conf im Repo-Root (714 Bytes)

server {
    listen 80;
    server_name _;

    root /var/www/html;
    index index.html;

    location /api/ {
        proxy_pass http://backend:8000/api/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /health {
        proxy_pass http://backend:8000/health;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location / {
        try_files $uri $uri/ /index.html;  # SPA-Fallback
    }
}

CRM-Anwendung:

  • 1:1 übernehmen für nginx-Service in docker-compose
  • Aber: CRM hat KEIN SPA mit client-side Routing in v1 (Alpine.js ist seitenbasiert, nicht SPA). try_files ist trotzdem OK, weil Alpine-Komponenten auf eigenen HTML-Pages liegen
  • proxy_pass http://crm-api:8000 (Service-Name an CRM anpassen)
  • /docs (Swagger) und /redoc zusätzlich via location exposen
  • /metrics für Prometheus (NFR-5, optional v1.1)

Pattern A-10: DB-Init mit Default-Usern (Bootstrap-Pattern)

Was: Beim Startup prüfen, ob Default-User existieren, ggf. anlegen. Wo: backend/main.py Zeilen 110-140

from passlib.context import CryptContext
import uuid

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# Init DB
init_db()

# Create default users if not exist
db = SessionLocal()
try:
    if not db.query(User).filter(User.username == "admin").first():
        db.add(User(id=str(uuid.uuid4()), username="admin",
                    password_hash=pwd_context.hash("admin"), role="admin"))
        db.add(User(id=str(uuid.uuid4()), username="user",
                    password_hash=pwd_context.hash("user"), role="user"))
        db.add(User(id=str(uuid.uuid4()), username="viewer",
                    password_hash=pwd_context.hash("viewer"), role="viewer"))
        db.commit()
finally:
    db.close()

CRM-Anwendung:

  • NICHT 1:1 übernehmen Wochenplaner hat hartcodierte Default-User (admin/admin, user/user, viewer/viewer) das ist ein SECURITY-ISSUE
  • CRM-Requirements (FR-1.1) verlangen Bootstrap-Registrierung: erste Registrierung nur möglich wenn users-Tabelle leer, danach 403
  • Pattern adaptieren: Statt Default-User anlegen, leere DB lassen und Bootstrap-Endpoint /api/auth/register exponieren mit Check if db.query(User).count() == 0: allow else: 403
  • uuid.uuid4() als String-ID übernehmen für v1 (einfacher als Auto-Increment für Replikation)

Pattern A-11: Rate-Limiting via DB-Tracking

Was: LoginAttempt-Tabelle trackt jeden Login-Versuch; 5 Failures in 10 Min → 429. Wo: backend/main.py Zeilen 175-200

# Rate-Limit-Check
recent_fails = db.query(LoginAttempt).filter(
    LoginAttempt.username == req.username,
    LoginAttempt.attempted_at > datetime.utcnow() - timedelta(minutes=10),
    LoginAttempt.success == False
).count()

if recent_fails >= 5:
    raise HTTPException(status_code=429, detail="Zu viele Fehlversuche. Bitte 10 Minuten warten.")

# Nach Verify:
if not user or not pwd_context.verify(req.password, user.password_hash):
    db.add(LoginAttempt(username=req.username, success=False))
    db.commit()
    raise HTTPException(status_code=401, detail="Ungültige Zugangsdaten")

db.add(LoginAttempt(username=req.username, success=True))
db.commit()

CRM-Anwendung:

  • Pattern 1:1 übernehmen, aber:
    • NFR-2 fordert 60 req/min/IP (SlowAPI oder ähnliches) Wochenplaner-Lösung ist Username-basiert, CRM sollte IP-basiert (oder beides)
    • Tabelle login_attempts mit Cleanup-Job (alle 24h alte Einträge löschen)
    • IP-Tracking: request.client.host zusätzlich speichern
  • Alternative: slowapi-Lib nutzen statt Custom-Tracking (moderneres Python-Idiom)

Pattern A-12: Health-Endpoint

Was: Minimaler /health mit Timestamp. Wo: backend/main.py Zeile 290

@app.get("/health")
def health():
    return {"status": "ok", "time": datetime.utcnow().isoformat()}

CRM-Anwendung:

  • 1:1 übernehmen, erweitern um DB-Connection-Check (NFR-5: 503 wenn DB down):
# CRM app/api/v1/health.py
@router.get("/health")
async def health(db: AsyncSession = Depends(get_db)):
    try:
        await db.execute(text("SELECT 1"))
        return {"status": "ok", "db": "ok", "time": datetime.utcnow().isoformat()}
    except Exception as e:
        raise HTTPException(status_code=503, detail=f"DB down: {e}")

Section B: Rentman-Clone-Repo

Status: NICHT GEFUNDEN via forgejo-Suche (Such-Queries: rentman, fastapi-crm) Suchergebnis: Beide Queries lieferten data: [] (0 Repositories) Konsequenz: Kein zusätzlicher Pattern-Reuse möglich. CRM-Implementation basiert nur auf wochenplaner-Patterns + 02-architecture.md Specs + 01-requirements.md Akzeptanzkriterien.

Alternative Pattern-Quellen (NICHT genutzt, da außerhalb des Auftrags):

  • Eigene FastAPI-Projekte (z.B. persönliche Templates) nicht in scope
  • Public Open-Source-CRMs (Twenty, EspoCRM) haben komplett anderen Stack (TypeORM/Node)

Section C: Pattern-Mapping auf CRM-Tasks

Pattern (aus wochenplaner) CRM-Phase CRM-Task-ID CRM-Datei-Pfad Änderungsbedarf
A-1 App-Init + CORS + Static-Mount 4a main-app app/main.py allow_origins Whitelist; Static-Mount unter /static; Frontend-Pages separat
A-2 JWT-Setup (python-jose, HS256) 4a core-modules app/core/security.py Expiry 24h statt 7 Tage; ≥32 Zeichen Secret; token_expired Hint
A-3 bcrypt + passlib 4a core-modules app/core/security.py 1:1 übernehmen, Rounds 12 OK
A-4 get_current_user + require_role 4a core-modules, auth-routes app/core/deps.py User-Model-Return statt dict; Rollen admin/manager/rep; Query-Param-Fallback prüfen
A-5 get_db() (sync) 4a core-modules app/core/db.py Komplett async umschreiben mit AsyncSession
A-6 SQLAlchemy-Engine + Session 4a core-modules app/core/db.py async-Engine; URL-Switch SQLite/Postgres; Mapped[]-Types 2.0
A-7 Dockerfile python:3.12-slim 4d dockerfile Dockerfile 1:1 + Multi-Stage + Non-Root + alembic-migrate-Start
A-8 docker-compose Multi-Container 4d docker-compose docker-compose.yml Service-Namen crm-api/crm-web/crm-db; Postgres-Service; SERVICE_BASE64_64-ENV
A-9 nginx Reverse-Proxy + SPA 4d docker-compose (frontend) frontend/nginx.conf Service-Name crm-api; /docs + /metrics exposen
A-10 DB-Init Default-User 4a auth-routes app/api/v1/auth.py NICHT übernehmen (Security); Bootstrap-Register-Endpoint mit User-count-Check
A-11 Rate-Limiting DB-Tracking 4a auth-routes app/services/auth_service.py IP+Username-basiert; 60 req/min NFR-2; Cleanup-Job
A-12 Health-Endpoint 4a health-route app/api/v1/health.py + DB-Connection-Check (NFR-5)
Static-Files-Mount (Frontend) 4c frontend-foundation app/main.py (mount) + webui/ Mount unter /static; HTML-Pages pro Feature
nginx-SPA-Fallback 4c frontend-foundation frontend/nginx.conf 1:1 OK, aber CRM nicht zwingend SPA
CORS-Wildcard 4a main-app app/main.py MUSS ersetzt werden durch Whitelist (NFR-2)
bcrypt 4.0.1 pinning 4a core-modules backend/requirements.txt Versionen exakt pinnen (sqlalchemy 2.0.35, bcrypt 4.0.1, passlib 1.7.4)
LoginAttempt-Table 4a models-base (optional in 4a) app/models/login_attempt.py Optional v1, ggf. erst v1.1

Section D: Risiken und Lessons Learned

Risiko 1: Sync-SQLAlchemy → Async-Migration

Wochenplaner-Pattern: Sync-Engine + sync-Session CRM-Anforderung: Async SQLAlchemy 2.0 + aiosqlite (Dev) / asyncpg (Prod) Lesson: Sync-Code aus wochenplaner (alle Endpoint-Handler, alle DB-Queries) MUSS in CRM async umgeschrieben werden. Pattern db.query()select() + await session.execute(). Risiko: sehr viele Stellen, Fehleranfälligkeit bei vergessenen await. Mitigation: Implementation-Engineer muss async-Tests (pytest-asyncio) früh schreiben.

Risiko 2: Kein Alembic im wochenplaner

Wochenplaner-Pattern: init_db() ruft Base.metadata.create_all(engine) auf keine Migrations. CRM-Anforderung: Alembic mit async-env.py, Migrationen 0001_init, 0002_business_entities. Lesson: CRM kann wochenplaner-Pattern NICHT für Schema-Migration nutzen. Neuer Setup nötig. Mitigation: alembic init -t async mit asyncpg/aiosqlite-URL.

Risiko 3: Hardcoded Default-User (admin/admin)

Wochenplaner-Pattern: Beim Start werden admin/admin123, user/user, viewer/viewer auto-erzeugt. CRM-Anforderung: FR-1.1: Bootstrap-Registrierung NUR wenn User-Tabelle leer. Lesson: 1:1-Übernahme wäre SECURITY-INCIDENT. Wochenplaner hat Default-User in Production weil app.run() immer beim Start ausgeführt wird. Mitigation: CRM-Implementation MUSS Pattern A-10 verwerfen und Bootstrap-Endpoint mit if count == 0 implementieren.

Risiko 4: CORS-Wildcard erlaubt alles

Wochenplaner-Pattern: allow_origins=["*"] CRM-Anforderung: NFR-2: Explizite CORS-Whitelist. Lesson: 1:1-Übernahme verletzt NFR-2. Mitigation: CRM main.py nutzt allow_origins=[os.environ.get("CORS_ORIGINS", "https://crm.media-on.de")].split(",").

Risiko 5: JWT-Secret-Fallback in Production

Wochenplaner-Pattern: os.environ.get("JWT_SECRET", "wochenplaner-jwt-secret-2024") Fallback hardcoded. CRM-Anforderung: NFR-2: AUTH_SECRET ≥32 Zeichen aus Coolify Env-Vars. Lesson: Fallback in Production ist SECURITY-RISK. Bei vergessenem ENV-Var läuft App mit known Secret. Mitigation: CRM config.py (Pydantic-Settings) MUSS AUTH_SECRET: str = Field(..., min_length=32) kein Default.

Risiko 6: bcrypt-Version-Inkompatibilität

Wochenplaner-Pattern: bcrypt==4.0.1 exakt gepinnt. CRM-Anforderung: bcrypt für Passwort-Hashing (NFR-2 erlaubt bcrypt als Fallback zu argon2id). Lesson: bcrypt 4.1+ hat __about__-Attribute-Änderung, die passlib bricht. 4.0.1 ist letzte stable Version für passlib. Mitigation: CRM-requirements.txt MUSS bcrypt==4.0.1 exakt pinnen + passlib[bcrypt]==1.7.4.

Risiko 7: SQLAlchemy 2.0 Mixed-Style

Wochenplaner-Pattern: Column(String) ohne Mapped[]-Type-Annotations. CRM-Anforderung: SQLAlchemy 2.0 mit modernem Mapped[T] + mapped_column() Style. Lesson: Wochenplaner nutzt 1.x-Style. CRM muss komplett 2.0-Style schreiben für mypy-strict-Compliance (NFR-4). Mitigation: Codebase-Explorer-Output: CRM-Models komplett neu schreiben, kein Copy-Paste aus wochenplaner/models.py.

Risiko 8: Statisches Volume statt PostgreSQL

Wochenplaner-Pattern: wochenplaner_data:/app/data mit SQLite. CRM-Anforderung: PostgreSQL als Production-DB (NFR-3, horizontale Skalierung). Lesson: SQLite-Limit (single-writer) macht horizontale Skalierung unmöglich. Volume-Pattern funktioniert nur für Single-Instance. Mitigation: CRM docker-compose MUSS PostgreSQL-Service + Healthcheck + Backup-Volume enthalten. SQLite nur für Dev (aiosqlite:///./dev.db).

Risiko 9: Domain-URL ohne Port

Wochenplaner-Realität: Domain https://reinigung.media-on.de:80 mit Port (Memory-regel) CRM-Anforderung: Domain crm.media-on.de:80 mit Port (selbe Regel) Lesson: User-Definitive-Rule: Port MUSS in Coolify-Domain-URL. Mitigation: Implementation-Engineer MUSS bei Coolify-Service-Setup urls: ["https://crm.media-on.de:80"] setzen (PATCH /api/v1/services/{uuid}).

Risiko 10: LoginAttempt-Persistenz vs. Cleanup

Wochenplaner-Pattern: LoginAttempt-Tabelle wächst unbegrenzt. CRM-Anforderung: NFR-4 Maintainability. Lesson: Kein Cleanup-Job definiert → Tabelle wächst → Performance-Degradation. Mitigation: CRM-Implementation mit Cleanup-Task (v1.1) oder TTL-Index.


Section E: Empfehlung für Phase 4

E-1: 1:1 übernehmbare Files (mit kleinen Anpassungen)

  1. backend/Dockerfile.backend → CRM Dockerfile (Pattern A-7)
    • Anpassungen: main:appapp.main:app, Multi-Stage-Build, Non-Root-User, alembic-Migration vor uvicorn
  2. docker-compose.yml → CRM docker-compose.yml (Pattern A-8)
    • Anpassungen: Service-Namen crm-api/crm-web/crm-db, Postgres-Service hinzu, DATABASE_URL und JWT_SECRET als SERVICE_BASE64_64
  3. nginx.conf → CRM frontend/nginx.conf (Pattern A-9)
    • Anpassungen: proxy_pass http://crm-api:8000, /docs + /metrics exposen
  4. requirements.txt → CRM backend/requirements.txt (Pinning-Liste)
    • Anpassungen: pydantic 2.7.0 → 2.9+ (aktuelle Sicherheitspatches), alembic + asyncpg/aiosqlite + pytest + pytest-asyncio + httpx + slowapi ergänzen

E-2: 1:1 übernehmbare Code-Snippets (in andere Files integrieren)

  1. JWT-Encode/Decode-Funktionen (Pattern A-2) → app/core/security.py
  2. CryptContext-Setup (Pattern A-3) → app/core/security.py
  3. require_role Factory-Pattern (Pattern A-4) → app/core/deps.py
  4. Rate-Limiting-Logik (Pattern A-11) → app/services/auth_service.py (erweitert um IP-Tracking)
  5. Health-Endpoint-Logik (Pattern A-12) → app/api/v1/health.py (erweitert um DB-Check)

E-3: Komplett neu zu schreibende Files

  1. app/core/db.py (Async SQLAlchemy + Alembic-kompatibel) Pattern A-5+6 nur als Referenz
  2. app/models/base.py (Base + TimestampMixin + SoftDeleteMixin + OrgScopedMixin) wochenplaner hat nur 1.x-Style
  3. app/models/org.py + app/models/user.py (mit org_id-FK) wochenplaner hat keine Org-Trennung
  4. Alle Service-Layer-Files (app/services/*_service.py) wochenplaner hat keinen Service-Layer
  5. Alle Schema-Files (app/schemas/*) wochenplaner hat nur inline Pydantic-Models in main.py
  6. app/api/v1/*.py Router wochenplaner hat alles in einem 12.9 KB main.py
  7. alembic/env.py + alembic/versions/0001_init.py wochenplaner hat keine Alembic
  8. app/core/config.py (Pydantic-Settings) wochenplaner nutzt rohe os.environ.get
  9. Frontend-Files (webui/index.html, webui/app.html, webui/js/*, webui/css/*) wochenplaner-Frontend nutzt Vanilla-JS, CRM braucht Alpine.js + Tailwind CDN

E-4: ENV-Vars, die schon im wochenplaner-Coolify funktionieren

Aus docker-compose.yml und Memory-Analyse:

ENV-Var wochenplaner-Wert CRM-Anwendung
PORT 80 (intern) 8000 (intern, dann via nginx exposed)
JWT_SECRET ${SERVICE_BASE64_64_JWT:-fallback} ${SERVICE_BASE64_64_JWT} (KEIN Fallback)
JWT_SECRET_MIN_LENGTH 32 (Pydantic-Validation)
DATABASE_URL (SQLite intern) ${SERVICE_BASE64_32_DB_URL} (PostgreSQL)
CORS_ORIGINS * (hardcoded) https://crm.media-on.de:80 (Whitelist)
ENV production / development
LOG_LEVEL INFO / DEBUG

Coolify-Setup-Empfehlung für CRM:

  • 3 Services in Coolify: crm-api (FastAPI), crm-web (nginx), crm-db (PostgreSQL)
  • Service-UUIDs: Implementation-Engineer MUSS nach Coolify-Create PATCH /api/v1/services/{uuid} mit urls: ["https://crm.media-on.de:80"] aufrufen
  • Domain: crm.media-on.de:80 (Port zwingend)
  • SERVICE_BASE64_64_JWT, SERVICE_BASE64_32_DB_URL werden auto-generiert von Coolify

Zusammenfassung

Metrik Wert
Sektionen 5 (A, B, C, D, E)
Dokumentierte Patterns (Section A) 12 (A-1 bis A-12)
CRM-Tasks in Mapping-Tabelle (Section C) 17 Zeilen
Risiken/Lessons Learned (Section D) 10
Empfehlungen (Section E) 4 Sub-Sektionen
Patterns 1:1 übernehmbar 4 Files + 5 Code-Snippets
Patterns neu zu schreiben 9 File-Kategorien
Wochenplaner als Quelle analysiert (forgejo, 6 Files gelesen)
Rentman-clone als Quelle NICHT GEFUNDEN
Empfehlung GO für Phase 4a (mit expliziten Warnungen zu Risiken 3, 4, 5, 8)

Offene Punkte für Implementation-Engineer (Phase 4a):

  1. Soll python-jose (wochenplaner-konsistent) ODER PyJWT (moderneres Python-Idiom) genutzt werden? Memory sagt python-jose, also beibehalten
  2. SQLite-only für Dev ODER parallel PostgreSQL-Dev? 02-architecture sagt SQLite-Dev, also beibehalten
  3. CSP-Header (R-5 Mitigation) im nginx.conf oder im FastAPI-Middleware? 02-architecture nennt main.py (Middleware), also dort
  4. Soll LoginAttempt-Tabelle in v1 oder erst v1.1 kommen? Phase 4a nicht zwingend