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

202 lines
8.0 KiB
Markdown

# 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
```python
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
```python
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
```python
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)
```python
@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)
```python
@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