Files
crm-system/specs/current/design.md
T

8.0 KiB

leocrm — Design (v0.1, Konkrete Specs)

1. Verzeichnisstruktur

leocrm/
├── README.md
├── LICENSE
├── .gitignore
├── pyproject.toml          # Projekt-Metadaten, dependencies
├── requirements.txt        # Pinned-Versionen für Reproduzierbarkeit
├── Dockerfile              # Single-Stage, python:3.11-slim
├── docker-compose.yml      # Coolify-kompatibel
├── .dockerignore
├── .env.example            # LEOCRM_SECRET_KEY, LEOCRM_DB_PATH
├── docs/
│   ├── architecture.md     # (siehe architecture.md)
│   ├── test_report.md      # wird in B7 erzeugt
│   ├── runtime_report.md   # wird in B7 erzeugt
│   └── api.md              # Auto-generierte OpenAPI-Übersicht (manuell)
├── deploy/
│   ├── env.md              # ENV-Variablen-Doku
│   ├── healthcheck.md      # Coolify-Healthcheck-Config
│   ├── runbook.md          # Runbook (Start/Stop/Logs/Restart)
│   └── rollback.md         # Rollback-Procedure
├── app/
│   ├── __init__.py
│   ├── main.py             # FastAPI-App-Factory, Middleware, Router-Mount
│   ├── config.py           # Settings (pydantic-settings)
│   ├── db/
│   │   ├── __init__.py
│   │   ├── models.py       # User, Company, Contact
│   │   ├── session.py      # engine, SessionLocal, get_db
│   │   └── init_db.py      # create_all + seed
│   ├── schemas/
│   │   ├── __init__.py
│   │   ├── auth.py         # LoginIn
│   │   ├── company.py      # CompanyIn, CompanyOut
│   │   └── contact.py      # ContactIn, ContactOut
│   ├── services/
│   │   ├── __init__.py
│   │   ├── auth_service.py
│   │   ├── company_service.py
│   │   └── contact_service.py
│   ├── routes/
│   │   ├── __init__.py
│   │   ├── html_routes.py
│   │   └── api_routes.py
│   ├── templates/
│   │   ├── base.html       # Layout: Navbar + Container + Footer
│   │   ├── login.html
│   │   ├── dashboard.html
│   │   ├── companies/
│   │   │   ├── list.html
│   │   │   ├── form.html   # new + edit
│   │   │   └── detail.html
│   │   └── contacts/
│   │       ├── form.html
│   │       └── detail.html
│   └── static/
│       └── style.css       # Minimal-CSS, kein Build-Step
└── tests/
    ├── __init__.py
    ├── conftest.py         # TestClient + DB-Fixture (sqlite:///:memory:)
    ├── test_health.py
    ├── test_auth.py
    ├── test_companies_api.py
    ├── test_contacts_api.py
    └── test_html_smoke.py  # Light HTML-Tests (Status, Redirects)

2. Konkrete Funktions-Signaturen

app/services/auth_service.py

def hash_password(plain: str) -> str: ...
def verify_password(plain: str, hashed: str) -> bool: ...
def authenticate_user(db: Session, username: str, password: str) -> User | None: ...

app/services/company_service.py

def list_companies(db: Session, q: str | None = None, limit: int = 50, offset: int = 0) -> list[Company]: ...
def get_company(db: Session, company_id: int) -> Company | None: ...
def create_company(db: Session, data: CompanyIn) -> Company: ...
def update_company(db: Session, company_id: int, data: CompanyIn) -> Company | None: ...
def delete_company(db: Session, company_id: int) -> bool: ...  # cascade
def count_companies(db: Session) -> int: ...

app/services/contact_service.py

def list_contacts(db: Session, company_id: int | None = None, limit: int = 50, offset: int = 0) -> list[Contact]: ...
def get_contact(db: Session, contact_id: int) -> Contact | None: ...
def create_contact(db: Session, data: ContactIn) -> Contact: ...  # Validierung: company muss existieren
def update_contact(db: Session, contact_id: int, data: ContactIn) -> Contact | None: ...
def delete_contact(db: Session, contact_id: int) -> bool: ...
def count_contacts(db: Session) -> int: ...

app/routes/api_routes.py (Auszug)

@router.get("/api/health")
def health(db: Session = Depends(get_db)) -> dict: ...

@router.post("/api/auth/login")
def login(payload: LoginIn, request: Request, response: Response, db: Session = Depends(get_db)): ...
@router.post("/api/auth/logout")
def logout(request: Request): ...

@router.get("/api/companies")
def list_companies(q: str | None = None, limit: int = 50, offset: int = 0, db: Session = Depends(get_db)) -> list[CompanyOut]: ...
@router.post("/api/companies", status_code=201)
def create_company(payload: CompanyIn, db: Session = Depends(get_db)) -> CompanyOut: ...
@router.get("/api/companies/{cid}")
def get_company(cid: int, db: Session = Depends(get_db)) -> CompanyOut: ...
@router.patch("/api/companies/{cid}")
def update_company(cid: int, payload: CompanyIn, db: Session = Depends(get_db)) -> CompanyOut: ...
@router.delete("/api/companies/{cid}", status_code=204)
def delete_company(cid: int, db: Session = Depends(get_db)): ...
# Analog für /api/contacts

app/routes/html_routes.py (Auszug)

@router.get("/login")
def login_form(request: Request): ...  # 200 wenn nicht eingeloggt, sonst redirect /
@router.post("/login")
def login_submit(payload: ..., request: Request, db: Session = Depends(get_db)): ...
@router.post("/logout")
def logout(request: Request): ...
@router.get("/")
def dashboard(request: Request, db: Session = Depends(get_db)): ...  # Auth-Required
@router.get("/companies")
def companies_list(request: Request, q: str | None = None, db: Session = Depends(get_db)): ...
# ... analog /companies/new, /companies/{id}, /companies/{id}/edit, /companies/{id}/delete
# ... /contacts/new, /contacts/{id}, /contacts/{id}/edit, /contacts/{id}/delete

3. Middleware-Reihenfolge (app/main.py)

  1. SessionMiddleware(secret_key=..., https_only=in_prod)
  2. CORS nur in Dev (in Prod nicht nötig, da Same-Origin)
  3. Custom AuthRequiredMiddleware für HTML: prüft Session auf allen Pfaden außer ["/login", "/static/*", "/api/health", "/api/auth/login"]
  4. API-Auth prüft jede Route via Dependency current_user_required

4. Templates (Jinja2)

base.html

  • Bootstrap-5 via CDN (kein npm-Build, da Single-Page-Container)
  • Navbar: Logo "leocrm" + Links "Dashboard", "Firmen", "Kontakte" + User-Name + Logout-Form
  • Block content

login.html

  • Form mit username, password, Submit "Anmelden"
  • Fehlermeldung bei ?error=1

dashboard.html

  • 3 Cards: Anzahl Firmen, Anzahl Kontakte, letzte 5 Einträge
  • Link "Neue Firma" / "Neuer Kontakt"

companies/list.html

  • Tabelle mit Spalten Name, Stadt, Land, # Kontakte, Aktionen
  • Suchform oben (Input + Submit)
  • Pagination (limit/offset)
  • Button "Neue Firma"

companies/form.html

  • Felder: Name*, Straße, PLZ, Stadt, Land, E-Mail, Telefon, Web, Notizen
  • Submit + Cancel

companies/detail.html

  • Alle Felder als Definition-List
  • Liste der Kontakte als Tabelle
  • Button "Kontakt hinzufügen" (verlinkt auf /contacts/new?company_id={id})
  • Buttons "Bearbeiten", "Löschen"

contacts/form.html + detail.html

  • Analog, mit Dropdown/Input für company_id (Pflicht)

5. CSS (app/static/style.css)

  • Minimal, nur Layout-Feinschliff (Buttons-Padding, Card-Spacing, Tabellen-Hover)
  • Keine Custom-Designs, Bootstrap-Defaults reichen

6. ENV-Variablen

Variable Default Bedeutung
LEOCRM_SECRET_KEY (Dev-Fallback "dev-insecure-key") Session-Signing-Key; in Prod Pflicht
LEOCRM_DB_PATH /data/leocrm.db SQLite-Pfad
LEOCRM_SEED_ON_START true Wenn true, wird bei leerer DB geseedet

7. OpenAPI / Swagger

FastAPI generiert automatisch /docs (Swagger UI) und /openapi.json. Beide bleiben aktiv, auch im Prod-Mode (kein Auth auf /docs für v0.1, dokumentiert als bekannte Einschränkung; v0.2 mit Auth).

8. Logging

  • Uvicorn-Default-Logging (stdout) reicht für v0.1
  • Kein File-Logging, kein Sentry