27 KiB
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(Branchmaster) 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.pymounten 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_KEYMUSS mindestens 32 Zeichen haben (NFR-2: ≥32 Zeichen). Fallback in Prod ist VERBOTEN.verify_tokenerweitern um expired-vs-invalid Unterscheidung fürtoken_expiredHint (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.4undbcrypt==4.0.1exakt 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_userzurückgibt nichtdict, sondern ein User-Model (async DB-Load). Wochenplaner nutzt nur JWT-Payload (kein DB-Reload) – CRM sollte User neu laden, damitis_active/Soft-Delete greift.require_rolefü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=Falsebeibehalten, 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_threadentfällt komplett (async-Engine nutzt asyncpg/aiosqlite, kein Multi-Thread-Issue)Base = declarative_base()ersetzen durchBase = DeclarativeBase+ Type-Annotations (SQLAlchemy 2.0 Style)- Models: Spalten-Definitionen ohne
Mapped[](sync) → CRM mitMapped[...] = 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:appersetzen durchapp.main:app(CRM nutztapp/-Package)/app/datadurch 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 appuserfü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-apistattbackend - 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}
- Service-Name
- 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_filesist trotzdem OK, weil Alpine-Komponenten auf eigenen HTML-Pages liegen proxy_pass http://crm-api:8000(Service-Name an CRM anpassen)/docs(Swagger) und/redoczusätzlich vialocationexposen/metricsfü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/registerexponieren mit Checkif 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_attemptsmit Cleanup-Job (alle 24h alte Einträge löschen) - IP-Tracking:
request.client.hostzusä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)
backend/Dockerfile.backend→ CRMDockerfile(Pattern A-7)- Anpassungen:
main:app→app.main:app, Multi-Stage-Build, Non-Root-User, alembic-Migration vor uvicorn
- Anpassungen:
docker-compose.yml→ CRMdocker-compose.yml(Pattern A-8)- Anpassungen: Service-Namen
crm-api/crm-web/crm-db, Postgres-Service hinzu,DATABASE_URLundJWT_SECRETals SERVICE_BASE64_64
- Anpassungen: Service-Namen
nginx.conf→ CRMfrontend/nginx.conf(Pattern A-9)- Anpassungen:
proxy_pass http://crm-api:8000, /docs + /metrics exposen
- Anpassungen:
requirements.txt→ CRMbackend/requirements.txt(Pinning-Liste)- Anpassungen:
pydantic2.7.0 → 2.9+ (aktuelle Sicherheitspatches),alembic+asyncpg/aiosqlite+pytest+pytest-asyncio+httpx+slowapiergänzen
- Anpassungen:
E-2: 1:1 übernehmbare Code-Snippets (in andere Files integrieren)
- JWT-Encode/Decode-Funktionen (Pattern A-2) →
app/core/security.py - CryptContext-Setup (Pattern A-3) →
app/core/security.py - require_role Factory-Pattern (Pattern A-4) →
app/core/deps.py - Rate-Limiting-Logik (Pattern A-11) →
app/services/auth_service.py(erweitert um IP-Tracking) - Health-Endpoint-Logik (Pattern A-12) →
app/api/v1/health.py(erweitert um DB-Check)
E-3: Komplett neu zu schreibende Files
app/core/db.py(Async SQLAlchemy + Alembic-kompatibel) – Pattern A-5+6 nur als Referenzapp/models/base.py(Base + TimestampMixin + SoftDeleteMixin + OrgScopedMixin) – wochenplaner hat nur 1.x-Styleapp/models/org.py+app/models/user.py(mit org_id-FK) – wochenplaner hat keine Org-Trennung- Alle Service-Layer-Files (
app/services/*_service.py) – wochenplaner hat keinen Service-Layer - Alle Schema-Files (
app/schemas/*) – wochenplaner hat nur inline Pydantic-Models in main.py app/api/v1/*.pyRouter – wochenplaner hat alles in einem 12.9 KB main.pyalembic/env.py+alembic/versions/0001_init.py– wochenplaner hat keine Alembicapp/core/config.py(Pydantic-Settings) – wochenplaner nutzt roheos.environ.get- 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}miturls: ["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):
- Soll
python-jose(wochenplaner-konsistent) ODERPyJWT(moderneres Python-Idiom) genutzt werden? – Memory sagt python-jose, also beibehalten - SQLite-only für Dev ODER parallel PostgreSQL-Dev? – 02-architecture sagt SQLite-Dev, also beibehalten
- CSP-Header (R-5 Mitigation) im nginx.conf oder im FastAPI-Middleware? – 02-architecture nennt main.py (Middleware), also dort
- Soll
LoginAttempt-Tabelle in v1 oder erst v1.1 kommen? – Phase 4a nicht zwingend